@better-update/cli 0.37.0 → 0.39.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 +1715 -534
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import chalk from "chalk";
|
|
|
20
20
|
import { promisify } from "node:util";
|
|
21
21
|
import ignore from "ignore";
|
|
22
22
|
import os, { tmpdir } from "node:os";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
23
24
|
import { maxBy, uniqBy } from "es-toolkit";
|
|
24
25
|
import forge from "node-forge";
|
|
25
26
|
import { AndroidConfig } from "@expo/config-plugins";
|
|
@@ -28,14 +29,13 @@ import { ExpoRunFormatter } from "@expo/xcpretty";
|
|
|
28
29
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
29
30
|
import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
|
|
30
31
|
import qrcode from "qrcode-terminal";
|
|
31
|
-
import { fileURLToPath } from "node:url";
|
|
32
32
|
|
|
33
33
|
//#region \0rolldown/runtime.js
|
|
34
34
|
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.39.0";
|
|
39
39
|
|
|
40
40
|
//#endregion
|
|
41
41
|
//#region src/lib/interactive-mode.ts
|
|
@@ -425,11 +425,11 @@ const Ciphertext = Schema.String.pipe(Schema.minLength(1)).annotations({ descrip
|
|
|
425
425
|
const WrappedDek = Schema.String.pipe(Schema.minLength(1)).annotations({ description: "Base64 of the DEK wrapped under the org vault key" });
|
|
426
426
|
/**
|
|
427
427
|
* The secret kinds whose DEK is wrapped under the org vault key — the rows a
|
|
428
|
-
* rotation must re-wrap.
|
|
428
|
+
* rotation must re-wrap. Eight signing-credential tables plus `envVarValue` (one
|
|
429
429
|
* row per environment variable value revision). Provisioning profiles are
|
|
430
430
|
* plaintext and are deliberately absent.
|
|
431
431
|
*/
|
|
432
|
-
const CredentialType = Schema.Literal("appleDistributionCertificate", "applePushKey", "ascApiKey", "googleServiceAccountKey", "androidUploadKeystore", "envVarValue").annotations({ description: "Which encrypted-secret table a vault-key DEK re-wrap targets" });
|
|
432
|
+
const CredentialType = Schema.Literal("appleDistributionCertificate", "applePushKey", "applePushCertificate", "applePayCertificate", "applePassTypeCertificate", "ascApiKey", "googleServiceAccountKey", "androidUploadKeystore", "envVarValue").annotations({ description: "Which encrypted-secret table a vault-key DEK re-wrap targets" });
|
|
433
433
|
/**
|
|
434
434
|
* The client-encrypted envelope. Spread into each secret credential's upload
|
|
435
435
|
* body and download result alongside that credential's public metadata. The
|
|
@@ -665,6 +665,128 @@ var AppleDistributionCertificatesGroup = class extends HttpApiGroup.make("appleD
|
|
|
665
665
|
description: "Manage .p12 distribution certificates"
|
|
666
666
|
})) {};
|
|
667
667
|
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region ../../packages/api/src/domain/apple-pass-type-certificate.ts
|
|
670
|
+
/**
|
|
671
|
+
* Pass Type ID certificate (Wallet passes), bound to a Pass Type ID
|
|
672
|
+
* (`pass.*`). The library has no Pass Type ID API, so these are uploaded
|
|
673
|
+
* manually: the CLI seals the `.p12` (cert + key) and the server stores only the
|
|
674
|
+
* envelope + metadata.
|
|
675
|
+
*/
|
|
676
|
+
var ApplePassTypeCertificate = class extends Schema.Class("ApplePassTypeCertificate")({
|
|
677
|
+
id: Id,
|
|
678
|
+
organizationId: Id,
|
|
679
|
+
appleTeamId: Id,
|
|
680
|
+
passTypeIdentifier: Schema.String,
|
|
681
|
+
serialNumber: Schema.String,
|
|
682
|
+
validFrom: DateTimeString,
|
|
683
|
+
validUntil: DateTimeString,
|
|
684
|
+
createdAt: DateTimeString,
|
|
685
|
+
updatedAt: DateTimeString
|
|
686
|
+
}) {};
|
|
687
|
+
/** Client-encrypted upload: the `.p12` bytes + password are sealed into `ciphertext`. */
|
|
688
|
+
const UploadApplePassTypeCertificateBody = Schema.Struct({
|
|
689
|
+
id: Id,
|
|
690
|
+
...encryptedEnvelopeFields,
|
|
691
|
+
passTypeIdentifier: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
692
|
+
serialNumber: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
693
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
694
|
+
...appleTeamMetadataFields,
|
|
695
|
+
validFrom: DateTimeString,
|
|
696
|
+
validUntil: DateTimeString
|
|
697
|
+
});
|
|
698
|
+
const DeleteApplePassTypeCertificateResult = DeletedResult;
|
|
699
|
+
/** The encrypted envelope (relayed from R2) plus metadata; the CLI decrypts `ciphertext` to recover `{ p12Base64, p12Password }`. */
|
|
700
|
+
const DownloadApplePassTypeCertificateResult = Schema.Struct({
|
|
701
|
+
id: Id,
|
|
702
|
+
...encryptedEnvelopeFields,
|
|
703
|
+
passTypeIdentifier: Schema.String,
|
|
704
|
+
serialNumber: Schema.String,
|
|
705
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
706
|
+
validFrom: DateTimeString,
|
|
707
|
+
validUntil: DateTimeString
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region ../../packages/api/src/groups/apple-pass-type-certificates.ts
|
|
712
|
+
var ApplePassTypeCertificatesGroup = class extends HttpApiGroup.make("applePassTypeCertificates").add(HttpApiEndpoint.get("list", "/api/apple/pass-type-certificates").addSuccess(Schema.Struct({ items: Schema.Array(ApplePassTypeCertificate) })).annotateContext(OpenApi.annotations({
|
|
713
|
+
title: "List Apple Pass Type ID certificates",
|
|
714
|
+
description: "List Pass Type ID certificates for the organization"
|
|
715
|
+
}))).add(HttpApiEndpoint.post("upload", "/api/apple/pass-type-certificates").setPayload(UploadApplePassTypeCertificateBody).addSuccess(ApplePassTypeCertificate, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
716
|
+
title: "Upload Pass Type ID certificate",
|
|
717
|
+
description: "Upload a Wallet Pass Type ID .p12 certificate"
|
|
718
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/apple/pass-type-certificates/${idParam}`.addSuccess(DeleteApplePassTypeCertificateResult).annotateContext(OpenApi.annotations({
|
|
719
|
+
title: "Delete Pass Type ID certificate",
|
|
720
|
+
description: "Remove a stored Pass Type ID certificate"
|
|
721
|
+
}))).add(HttpApiEndpoint.get("download")`/api/apple/pass-type-certificates/${idParam}/download`.addSuccess(DownloadApplePassTypeCertificateResult).annotateContext(OpenApi.annotations({
|
|
722
|
+
title: "Download Pass Type ID certificate",
|
|
723
|
+
description: "Fetch the decrypted .p12 Pass Type ID certificate for local use (audit-logged)"
|
|
724
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
725
|
+
title: "Apple Pass Type ID Certificates",
|
|
726
|
+
description: "Manage Wallet Pass Type ID certificates"
|
|
727
|
+
})) {};
|
|
728
|
+
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region ../../packages/api/src/domain/apple-pay-certificate.ts
|
|
731
|
+
/**
|
|
732
|
+
* Apple Pay Payment Processing certificate, bound to a Merchant ID
|
|
733
|
+
* (`merchant.*`). The library cannot create these (no portal cert type), so they
|
|
734
|
+
* are uploaded manually: the CLI seals the `.p12` (cert + key) and the server
|
|
735
|
+
* stores only the envelope + metadata.
|
|
736
|
+
*/
|
|
737
|
+
var ApplePayCertificate = class extends Schema.Class("ApplePayCertificate")({
|
|
738
|
+
id: Id,
|
|
739
|
+
organizationId: Id,
|
|
740
|
+
appleTeamId: Id,
|
|
741
|
+
merchantIdentifier: Schema.String,
|
|
742
|
+
serialNumber: Schema.String,
|
|
743
|
+
validFrom: DateTimeString,
|
|
744
|
+
validUntil: DateTimeString,
|
|
745
|
+
createdAt: DateTimeString,
|
|
746
|
+
updatedAt: DateTimeString
|
|
747
|
+
}) {};
|
|
748
|
+
/** Client-encrypted upload: the `.p12` bytes + password are sealed into `ciphertext`. */
|
|
749
|
+
const UploadApplePayCertificateBody = Schema.Struct({
|
|
750
|
+
id: Id,
|
|
751
|
+
...encryptedEnvelopeFields,
|
|
752
|
+
merchantIdentifier: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
753
|
+
serialNumber: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
754
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
755
|
+
...appleTeamMetadataFields,
|
|
756
|
+
validFrom: DateTimeString,
|
|
757
|
+
validUntil: DateTimeString
|
|
758
|
+
});
|
|
759
|
+
const DeleteApplePayCertificateResult = DeletedResult;
|
|
760
|
+
/** The encrypted envelope (relayed from R2) plus metadata; the CLI decrypts `ciphertext` to recover `{ p12Base64, p12Password }`. */
|
|
761
|
+
const DownloadApplePayCertificateResult = Schema.Struct({
|
|
762
|
+
id: Id,
|
|
763
|
+
...encryptedEnvelopeFields,
|
|
764
|
+
merchantIdentifier: Schema.String,
|
|
765
|
+
serialNumber: Schema.String,
|
|
766
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
767
|
+
validFrom: DateTimeString,
|
|
768
|
+
validUntil: DateTimeString
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region ../../packages/api/src/groups/apple-pay-certificates.ts
|
|
773
|
+
var ApplePayCertificatesGroup = class extends HttpApiGroup.make("applePayCertificates").add(HttpApiEndpoint.get("list", "/api/apple/pay-certificates").addSuccess(Schema.Struct({ items: Schema.Array(ApplePayCertificate) })).annotateContext(OpenApi.annotations({
|
|
774
|
+
title: "List Apple Pay certificates",
|
|
775
|
+
description: "List Apple Pay payment processing certificates for the organization"
|
|
776
|
+
}))).add(HttpApiEndpoint.post("upload", "/api/apple/pay-certificates").setPayload(UploadApplePayCertificateBody).addSuccess(ApplePayCertificate, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
777
|
+
title: "Upload Apple Pay certificate",
|
|
778
|
+
description: "Upload an Apple Pay payment processing .p12 certificate"
|
|
779
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/apple/pay-certificates/${idParam}`.addSuccess(DeleteApplePayCertificateResult).annotateContext(OpenApi.annotations({
|
|
780
|
+
title: "Delete Apple Pay certificate",
|
|
781
|
+
description: "Remove a stored Apple Pay payment processing certificate"
|
|
782
|
+
}))).add(HttpApiEndpoint.get("download")`/api/apple/pay-certificates/${idParam}/download`.addSuccess(DownloadApplePayCertificateResult).annotateContext(OpenApi.annotations({
|
|
783
|
+
title: "Download Apple Pay certificate",
|
|
784
|
+
description: "Fetch the decrypted .p12 Apple Pay certificate for local use (audit-logged)"
|
|
785
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
786
|
+
title: "Apple Pay Certificates",
|
|
787
|
+
description: "Manage Apple Pay payment processing certificates"
|
|
788
|
+
})) {};
|
|
789
|
+
|
|
668
790
|
//#endregion
|
|
669
791
|
//#region ../../packages/api/src/domain/apple-provisioning-profile.ts
|
|
670
792
|
const DistributionType = Schema.Literal("APP_STORE", "AD_HOC", "ENTERPRISE", "DEVELOPMENT");
|
|
@@ -722,6 +844,71 @@ var AppleProvisioningProfilesGroup = class extends HttpApiGroup.make("appleProvi
|
|
|
722
844
|
description: "Manage .mobileprovision profiles (upload or generate)"
|
|
723
845
|
})) {};
|
|
724
846
|
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region ../../packages/api/src/domain/apple-push-certificate.ts
|
|
849
|
+
/**
|
|
850
|
+
* Legacy APNs Push Services SSL certificate (the `.cer`/`.p12` push cert, distinct
|
|
851
|
+
* from the modern `.p8` token key in `apple-push-key.ts`). Bound to a single App
|
|
852
|
+
* ID (`bundleIdentifier`) rather than a whole team. The production cert serves
|
|
853
|
+
* both the sandbox and production APNs environments.
|
|
854
|
+
*/
|
|
855
|
+
var ApplePushCertificate = class extends Schema.Class("ApplePushCertificate")({
|
|
856
|
+
id: Id,
|
|
857
|
+
organizationId: Id,
|
|
858
|
+
appleTeamId: Id,
|
|
859
|
+
bundleIdentifier: Schema.String,
|
|
860
|
+
serialNumber: Schema.String,
|
|
861
|
+
validFrom: DateTimeString,
|
|
862
|
+
validUntil: DateTimeString,
|
|
863
|
+
createdAt: DateTimeString,
|
|
864
|
+
updatedAt: DateTimeString
|
|
865
|
+
}) {};
|
|
866
|
+
/**
|
|
867
|
+
* Client-encrypted upload: the `.p12` bytes + password are sealed into
|
|
868
|
+
* `ciphertext` (the CLI parses the cert locally to fill the metadata below);
|
|
869
|
+
* the server stores the envelope and metadata and never sees the plaintext.
|
|
870
|
+
*/
|
|
871
|
+
const UploadApplePushCertificateBody = Schema.Struct({
|
|
872
|
+
id: Id,
|
|
873
|
+
...encryptedEnvelopeFields,
|
|
874
|
+
bundleIdentifier: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
875
|
+
serialNumber: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200)),
|
|
876
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
877
|
+
...appleTeamMetadataFields,
|
|
878
|
+
validFrom: DateTimeString,
|
|
879
|
+
validUntil: DateTimeString
|
|
880
|
+
});
|
|
881
|
+
const DeleteApplePushCertificateResult = DeletedResult;
|
|
882
|
+
/** The encrypted envelope (relayed from R2) plus server-visible metadata; the CLI decrypts `ciphertext` to recover `{ p12Base64, p12Password }`. */
|
|
883
|
+
const DownloadApplePushCertificateResult = Schema.Struct({
|
|
884
|
+
id: Id,
|
|
885
|
+
...encryptedEnvelopeFields,
|
|
886
|
+
bundleIdentifier: Schema.String,
|
|
887
|
+
serialNumber: Schema.String,
|
|
888
|
+
appleTeamIdentifier: AppleTeamIdentifier,
|
|
889
|
+
validFrom: DateTimeString,
|
|
890
|
+
validUntil: DateTimeString
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
//#endregion
|
|
894
|
+
//#region ../../packages/api/src/groups/apple-push-certificates.ts
|
|
895
|
+
var ApplePushCertificatesGroup = class extends HttpApiGroup.make("applePushCertificates").add(HttpApiEndpoint.get("list", "/api/apple/push-certificates").addSuccess(Schema.Struct({ items: Schema.Array(ApplePushCertificate) })).annotateContext(OpenApi.annotations({
|
|
896
|
+
title: "List Apple push certificates",
|
|
897
|
+
description: "List APNs push SSL certificates for the organization"
|
|
898
|
+
}))).add(HttpApiEndpoint.post("upload", "/api/apple/push-certificates").setPayload(UploadApplePushCertificateBody).addSuccess(ApplePushCertificate, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
899
|
+
title: "Upload push certificate",
|
|
900
|
+
description: "Upload an APNs Push Services .p12 SSL certificate"
|
|
901
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/apple/push-certificates/${idParam}`.addSuccess(DeleteApplePushCertificateResult).annotateContext(OpenApi.annotations({
|
|
902
|
+
title: "Delete push certificate",
|
|
903
|
+
description: "Remove a stored APNs push SSL certificate"
|
|
904
|
+
}))).add(HttpApiEndpoint.get("download")`/api/apple/push-certificates/${idParam}/download`.addSuccess(DownloadApplePushCertificateResult).annotateContext(OpenApi.annotations({
|
|
905
|
+
title: "Download push certificate",
|
|
906
|
+
description: "Fetch the decrypted .p12 push certificate for local use (audit-logged)"
|
|
907
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
908
|
+
title: "Apple Push Certificates",
|
|
909
|
+
description: "Manage APNs Push Services SSL certificates"
|
|
910
|
+
})) {};
|
|
911
|
+
|
|
725
912
|
//#endregion
|
|
726
913
|
//#region ../../packages/api/src/domain/apple-push-key.ts
|
|
727
914
|
const ApplePushKeyId = tenCharPortalId("Push Key ID");
|
|
@@ -2550,7 +2737,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
|
|
|
2550
2737
|
|
|
2551
2738
|
//#endregion
|
|
2552
2739
|
//#region ../../packages/api/src/api.ts
|
|
2553
|
-
var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(EnvironmentsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(RuntimesGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2740
|
+
var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(EnvironmentsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(RuntimesGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(ApplePushCertificatesGroup).add(ApplePayCertificatesGroup).add(ApplePassTypeCertificatesGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2554
2741
|
title: "Better Update Management API",
|
|
2555
2742
|
version: "1.0.0",
|
|
2556
2743
|
description: "Management API for OTA update publishing, deployment, and analytics"
|
|
@@ -3567,8 +3754,9 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
|
3567
3754
|
//#endregion
|
|
3568
3755
|
//#region src/services/version-check.ts
|
|
3569
3756
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/@better-update/cli/latest";
|
|
3570
|
-
const CACHE_TTL_MS =
|
|
3757
|
+
const CACHE_TTL_MS = 300 * 1e3;
|
|
3571
3758
|
const REFRESH_TIMEOUT_MS = 3e3;
|
|
3759
|
+
const FOREGROUND_TIMEOUT_MS = 1500;
|
|
3572
3760
|
var VersionCheck = class extends Context.Tag("cli/VersionCheck")() {};
|
|
3573
3761
|
const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
3574
3762
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -3588,6 +3776,20 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
3588
3776
|
checkedAt: parsed["checkedAt"]
|
|
3589
3777
|
};
|
|
3590
3778
|
});
|
|
3779
|
+
const fetchAndCache = (timeoutMs) => Effect.gen(function* () {
|
|
3780
|
+
const request = HttpClientRequest.get(NPM_REGISTRY_URL).pipe(HttpClientRequest.setHeader("accept", "application/json"));
|
|
3781
|
+
const response = yield* httpClient.execute(request);
|
|
3782
|
+
if (response.status < 200 || response.status >= 300) return;
|
|
3783
|
+
const body = yield* response.json;
|
|
3784
|
+
if (!isRecord$1(body) || typeof body["version"] !== "string") return;
|
|
3785
|
+
const latest = body["version"];
|
|
3786
|
+
yield* fs.makeDirectory(cacheDir, { recursive: true });
|
|
3787
|
+
yield* fs.writeFileString(cacheFile, `${JSON.stringify({
|
|
3788
|
+
latest,
|
|
3789
|
+
checkedAt: Date.now()
|
|
3790
|
+
}, null, 2)}\n`);
|
|
3791
|
+
return latest;
|
|
3792
|
+
}).pipe(Effect.timeout(timeoutMs), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
3591
3793
|
return {
|
|
3592
3794
|
cachedLatest: readCache.pipe(Effect.map((entry) => entry?.latest)),
|
|
3593
3795
|
cacheStale: readCache.pipe(Effect.map((entry) => {
|
|
@@ -3595,19 +3797,8 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
3595
3797
|
const elapsed = Date.now() - entry.checkedAt;
|
|
3596
3798
|
return elapsed < 0 || elapsed > CACHE_TTL_MS;
|
|
3597
3799
|
})),
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
const response = yield* httpClient.execute(request);
|
|
3601
|
-
if (response.status < 200 || response.status >= 300) return;
|
|
3602
|
-
const body = yield* response.json;
|
|
3603
|
-
if (!isRecord$1(body) || typeof body["version"] !== "string") return;
|
|
3604
|
-
const latest = body["version"];
|
|
3605
|
-
yield* fs.makeDirectory(cacheDir, { recursive: true });
|
|
3606
|
-
yield* fs.writeFileString(cacheFile, `${JSON.stringify({
|
|
3607
|
-
latest,
|
|
3608
|
-
checkedAt: Date.now()
|
|
3609
|
-
}, null, 2)}\n`);
|
|
3610
|
-
}).pipe(Effect.timeout(REFRESH_TIMEOUT_MS), Effect.catchAll(() => Effect.void))
|
|
3800
|
+
fetchLatest: fetchAndCache(FOREGROUND_TIMEOUT_MS),
|
|
3801
|
+
refreshCache: fetchAndCache(REFRESH_TIMEOUT_MS).pipe(Effect.asVoid)
|
|
3611
3802
|
};
|
|
3612
3803
|
}));
|
|
3613
3804
|
|
|
@@ -4535,6 +4726,79 @@ const writeEasJsonPatch = (projectRoot, patch) => Effect.gen(function* () {
|
|
|
4535
4726
|
return filePath;
|
|
4536
4727
|
});
|
|
4537
4728
|
/**
|
|
4729
|
+
* Default `build` profiles scaffolded by `init` / `build configure`. Mirrors the
|
|
4730
|
+
* EAS three-tier convention: `development` (dev-client, internal), `preview`
|
|
4731
|
+
* (internal QA) and `production` (store). Keep in sync with the build-profile
|
|
4732
|
+
* derivation in `build-profile.ts` (e.g. `distribution: "internal"` → ad-hoc).
|
|
4733
|
+
*/
|
|
4734
|
+
const DEFAULT_BUILD_PROFILES = {
|
|
4735
|
+
development: {
|
|
4736
|
+
developmentClient: true,
|
|
4737
|
+
distribution: "internal",
|
|
4738
|
+
channel: "development",
|
|
4739
|
+
environment: "development",
|
|
4740
|
+
android: { format: "apk" }
|
|
4741
|
+
},
|
|
4742
|
+
preview: {
|
|
4743
|
+
distribution: "internal",
|
|
4744
|
+
channel: "preview",
|
|
4745
|
+
environment: "preview",
|
|
4746
|
+
android: { format: "apk" }
|
|
4747
|
+
},
|
|
4748
|
+
production: {
|
|
4749
|
+
channel: "production",
|
|
4750
|
+
environment: "production",
|
|
4751
|
+
android: { format: "aab" }
|
|
4752
|
+
}
|
|
4753
|
+
};
|
|
4754
|
+
const DEFAULT_PROFILE_NAMES = [
|
|
4755
|
+
"development",
|
|
4756
|
+
"preview",
|
|
4757
|
+
"production"
|
|
4758
|
+
];
|
|
4759
|
+
/** Full default `eas.json` body (cli pin + the three default build profiles). */
|
|
4760
|
+
const DEFAULT_EAS_JSON = {
|
|
4761
|
+
cli: { version: ">= 7.0.0" },
|
|
4762
|
+
build: DEFAULT_BUILD_PROFILES
|
|
4763
|
+
};
|
|
4764
|
+
/**
|
|
4765
|
+
* Ensure `eas.json` carries the default build profiles. Creates the file with
|
|
4766
|
+
* the full default template when absent; otherwise tops up only the missing
|
|
4767
|
+
* default profiles, preserving every existing profile and top-level key
|
|
4768
|
+
* (`projectId`, `projectType`, `submit`, …). Never overwrites a profile that is
|
|
4769
|
+
* already defined — call sites wanting a hard reset write `DEFAULT_EAS_JSON`.
|
|
4770
|
+
*/
|
|
4771
|
+
const ensureDefaultBuildProfiles = (projectRoot) => Effect.gen(function* () {
|
|
4772
|
+
const existing = yield* readEasJsonRaw(projectRoot);
|
|
4773
|
+
if (existing === void 0) return {
|
|
4774
|
+
path: yield* writeEasJsonPatch(projectRoot, DEFAULT_EAS_JSON),
|
|
4775
|
+
action: "created",
|
|
4776
|
+
added: [...DEFAULT_PROFILE_NAMES]
|
|
4777
|
+
};
|
|
4778
|
+
const existingBuild = isRecord$1(existing["build"]) ? existing["build"] : {};
|
|
4779
|
+
const missing = DEFAULT_PROFILE_NAMES.filter((name) => !(name in existingBuild));
|
|
4780
|
+
if (missing.length === 0) return {
|
|
4781
|
+
path: easJsonPath(projectRoot),
|
|
4782
|
+
action: "noop",
|
|
4783
|
+
added: []
|
|
4784
|
+
};
|
|
4785
|
+
const additions = Object.fromEntries(missing.map((name) => [name, DEFAULT_BUILD_PROFILES[name]]));
|
|
4786
|
+
return {
|
|
4787
|
+
path: yield* writeEasJsonPatch(projectRoot, existing["cli"] === void 0 ? {
|
|
4788
|
+
cli: DEFAULT_EAS_JSON.cli,
|
|
4789
|
+
build: {
|
|
4790
|
+
...existingBuild,
|
|
4791
|
+
...additions
|
|
4792
|
+
}
|
|
4793
|
+
} : { build: {
|
|
4794
|
+
...existingBuild,
|
|
4795
|
+
...additions
|
|
4796
|
+
} }),
|
|
4797
|
+
action: "topped-up",
|
|
4798
|
+
added: missing
|
|
4799
|
+
};
|
|
4800
|
+
});
|
|
4801
|
+
/**
|
|
4538
4802
|
* Resolve the linked project id from `eas.json`'s top-level `projectId`, or
|
|
4539
4803
|
* `undefined` when the file is absent / has no usable value.
|
|
4540
4804
|
*/
|
|
@@ -19871,38 +20135,6 @@ const acquireBuildTempDir = Effect.gen(function* () {
|
|
|
19871
20135
|
return dir;
|
|
19872
20136
|
});
|
|
19873
20137
|
|
|
19874
|
-
//#endregion
|
|
19875
|
-
//#region src/lib/asc-credentials.ts
|
|
19876
|
-
/**
|
|
19877
|
-
* Fetch an ASC API key and decrypt its `.p8` PEM locally. The server is
|
|
19878
|
-
* zero-knowledge: `getCredentials` returns the encrypted envelope, so the CLI
|
|
19879
|
-
* unwraps it here before talking to Apple. The unlock passphrase is resolved
|
|
19880
|
-
* lazily — prompted for a file identity, none for the CI env key.
|
|
19881
|
-
*/
|
|
19882
|
-
const fetchAscCredentials = (api, ascApiKeyId) => Effect.gen(function* () {
|
|
19883
|
-
const data = yield* api.ascApiKeys.getCredentials({ path: { id: ascApiKeyId } });
|
|
19884
|
-
const { p8Pem } = yield* openFromDownload({
|
|
19885
|
-
session: yield* openVaultSessionInteractive(api),
|
|
19886
|
-
credentialType: "asc-api-key",
|
|
19887
|
-
downloaded: {
|
|
19888
|
-
id: data.ascApiKeyId,
|
|
19889
|
-
ciphertext: data.ciphertext,
|
|
19890
|
-
wrappedDek: data.wrappedDek,
|
|
19891
|
-
vaultVersion: data.vaultVersion,
|
|
19892
|
-
keyId: data.keyId,
|
|
19893
|
-
issuerId: data.issuerId
|
|
19894
|
-
}
|
|
19895
|
-
});
|
|
19896
|
-
if (typeof p8Pem !== "string") return yield* new IdentityError({ message: `Decrypted ASC API key ${ascApiKeyId} is missing its .p8 PEM.` });
|
|
19897
|
-
return {
|
|
19898
|
-
ascApiKeyId: data.ascApiKeyId,
|
|
19899
|
-
keyId: data.keyId,
|
|
19900
|
-
issuerId: data.issuerId,
|
|
19901
|
-
appleTeamIdentifier: data.appleTeamIdentifier,
|
|
19902
|
-
p8Pem
|
|
19903
|
-
};
|
|
19904
|
-
});
|
|
19905
|
-
|
|
19906
20138
|
//#endregion
|
|
19907
20139
|
//#region src/lib/google-play.ts
|
|
19908
20140
|
var GooglePlayAuthError = class extends Schema.TaggedError()("GooglePlayAuthError", {
|
|
@@ -20135,18 +20367,29 @@ const uploadBundle = (params) => Effect.gen(function* () {
|
|
|
20135
20367
|
cause
|
|
20136
20368
|
})));
|
|
20137
20369
|
});
|
|
20370
|
+
/**
|
|
20371
|
+
* Build the track release object. Google Play accepts `userFraction` only for a
|
|
20372
|
+
* staged (`inProgress`) rollout and rejects it for `completed`/`draft`/`halted`,
|
|
20373
|
+
* so the fraction is gated on the status rather than passed through blindly.
|
|
20374
|
+
*/
|
|
20375
|
+
const buildTrackRelease = (params) => ({
|
|
20376
|
+
status: params.releaseStatus,
|
|
20377
|
+
versionCodes: [String(params.versionCode)],
|
|
20378
|
+
...compact({
|
|
20379
|
+
userFraction: params.releaseStatus === "inProgress" ? toOptional(params.rollout) : void 0,
|
|
20380
|
+
releaseNotes: params.releaseNotes ? [{
|
|
20381
|
+
language: "en-US",
|
|
20382
|
+
text: params.releaseNotes
|
|
20383
|
+
}] : void 0
|
|
20384
|
+
})
|
|
20385
|
+
});
|
|
20138
20386
|
const updateTrack = (params) => {
|
|
20139
|
-
const release = {
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
20143
|
-
|
|
20144
|
-
|
|
20145
|
-
language: "en-US",
|
|
20146
|
-
text: params.releaseNotes
|
|
20147
|
-
}] : void 0
|
|
20148
|
-
})
|
|
20149
|
-
};
|
|
20387
|
+
const release = buildTrackRelease({
|
|
20388
|
+
releaseStatus: params.releaseStatus,
|
|
20389
|
+
versionCode: params.versionCode,
|
|
20390
|
+
rollout: params.rollout,
|
|
20391
|
+
releaseNotes: params.releaseNotes
|
|
20392
|
+
});
|
|
20150
20393
|
return callJsonRaw({
|
|
20151
20394
|
url: `${ANDROID_PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(params.packageName)}/edits/${encodeURIComponent(params.editId)}/tracks/${encodeURIComponent(params.track)}`,
|
|
20152
20395
|
method: "PUT",
|
|
@@ -20165,6 +20408,624 @@ const commitEdit = (params) => callJsonRaw({
|
|
|
20165
20408
|
label: "edits.commit"
|
|
20166
20409
|
});
|
|
20167
20410
|
|
|
20411
|
+
//#endregion
|
|
20412
|
+
//#region src/lib/asc-credentials.ts
|
|
20413
|
+
/**
|
|
20414
|
+
* Fetch an ASC API key and decrypt its `.p8` PEM locally. The server is
|
|
20415
|
+
* zero-knowledge: `getCredentials` returns the encrypted envelope, so the CLI
|
|
20416
|
+
* unwraps it here before talking to Apple. The unlock passphrase is resolved
|
|
20417
|
+
* lazily — prompted for a file identity, none for the CI env key.
|
|
20418
|
+
*/
|
|
20419
|
+
const fetchAscCredentials = (api, ascApiKeyId) => Effect.gen(function* () {
|
|
20420
|
+
const data = yield* api.ascApiKeys.getCredentials({ path: { id: ascApiKeyId } });
|
|
20421
|
+
const { p8Pem } = yield* openFromDownload({
|
|
20422
|
+
session: yield* openVaultSessionInteractive(api),
|
|
20423
|
+
credentialType: "asc-api-key",
|
|
20424
|
+
downloaded: {
|
|
20425
|
+
id: data.ascApiKeyId,
|
|
20426
|
+
ciphertext: data.ciphertext,
|
|
20427
|
+
wrappedDek: data.wrappedDek,
|
|
20428
|
+
vaultVersion: data.vaultVersion,
|
|
20429
|
+
keyId: data.keyId,
|
|
20430
|
+
issuerId: data.issuerId
|
|
20431
|
+
}
|
|
20432
|
+
});
|
|
20433
|
+
if (typeof p8Pem !== "string") return yield* new IdentityError({ message: `Decrypted ASC API key ${ascApiKeyId} is missing its .p8 PEM.` });
|
|
20434
|
+
return {
|
|
20435
|
+
ascApiKeyId: data.ascApiKeyId,
|
|
20436
|
+
keyId: data.keyId,
|
|
20437
|
+
issuerId: data.issuerId,
|
|
20438
|
+
appleTeamIdentifier: data.appleTeamIdentifier,
|
|
20439
|
+
p8Pem
|
|
20440
|
+
};
|
|
20441
|
+
});
|
|
20442
|
+
|
|
20443
|
+
//#endregion
|
|
20444
|
+
//#region src/lib/apple-pem.ts
|
|
20445
|
+
const PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
|
|
20446
|
+
const PEM_FOOTER = "-----END PRIVATE KEY-----";
|
|
20447
|
+
const pemToPkcs8Der = (pem) => {
|
|
20448
|
+
const normalized = pem.replaceAll("\r\n", "\n").trim();
|
|
20449
|
+
const start = normalized.indexOf(PEM_HEADER);
|
|
20450
|
+
const end = normalized.indexOf(PEM_FOOTER);
|
|
20451
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
20452
|
+
const body = normalized.slice(start + 27, end).replaceAll(/\s+/gu, "").trim();
|
|
20453
|
+
if (body.length === 0) return null;
|
|
20454
|
+
try {
|
|
20455
|
+
return fromBase64(body);
|
|
20456
|
+
} catch {
|
|
20457
|
+
return null;
|
|
20458
|
+
}
|
|
20459
|
+
};
|
|
20460
|
+
|
|
20461
|
+
//#endregion
|
|
20462
|
+
//#region src/lib/apple-asc-jwt.ts
|
|
20463
|
+
var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
|
|
20464
|
+
const MAX_JWT_LIFETIME_SECONDS = 1200;
|
|
20465
|
+
const asArrayBuffer = (bytes) => {
|
|
20466
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
20467
|
+
new Uint8Array(buffer).set(bytes);
|
|
20468
|
+
return buffer;
|
|
20469
|
+
};
|
|
20470
|
+
const signAscJwt = (credentials) => Effect.gen(function* () {
|
|
20471
|
+
const der = pemToPkcs8Der(credentials.p8Pem);
|
|
20472
|
+
if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
|
|
20473
|
+
const header = {
|
|
20474
|
+
alg: "ES256",
|
|
20475
|
+
kid: credentials.keyId,
|
|
20476
|
+
typ: "JWT"
|
|
20477
|
+
};
|
|
20478
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
20479
|
+
const payload = {
|
|
20480
|
+
iss: credentials.issuerId,
|
|
20481
|
+
iat: now,
|
|
20482
|
+
exp: now + MAX_JWT_LIFETIME_SECONDS,
|
|
20483
|
+
aud: "appstoreconnect-v1"
|
|
20484
|
+
};
|
|
20485
|
+
const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
20486
|
+
const key = yield* Effect.tryPromise({
|
|
20487
|
+
try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
|
|
20488
|
+
name: "ECDSA",
|
|
20489
|
+
namedCurve: "P-256"
|
|
20490
|
+
}, false, ["sign"]),
|
|
20491
|
+
catch: (cause) => new AppleAuthError({ cause })
|
|
20492
|
+
});
|
|
20493
|
+
const signature = yield* Effect.tryPromise({
|
|
20494
|
+
try: async () => crypto.subtle.sign({
|
|
20495
|
+
name: "ECDSA",
|
|
20496
|
+
hash: "SHA-256"
|
|
20497
|
+
}, key, new TextEncoder().encode(signingInput)),
|
|
20498
|
+
catch: (cause) => new AppleAuthError({ cause })
|
|
20499
|
+
});
|
|
20500
|
+
return `${signingInput}.${toBase64Url(new Uint8Array(signature))}`;
|
|
20501
|
+
});
|
|
20502
|
+
|
|
20503
|
+
//#endregion
|
|
20504
|
+
//#region src/lib/apple-asc-client.ts
|
|
20505
|
+
/**
|
|
20506
|
+
* App Store Connect REST client authenticated with an ASC **API key** — a JWT
|
|
20507
|
+
* signed from a `.p8` private key (see `apple-asc-jwt.ts`). Credentials are
|
|
20508
|
+
* resolved non-interactively from the server (`fetchAscCredentials`), so this
|
|
20509
|
+
* powers headless flows: build-credential resolution, provisioning-profile
|
|
20510
|
+
* generation, and device sync.
|
|
20511
|
+
*
|
|
20512
|
+
* Intentionally NOT built on `@expo/apple-utils`: that library authenticates
|
|
20513
|
+
* via an interactive Apple-ID **cookie session** (username/password + 2FA, see
|
|
20514
|
+
* `services/apple-auth.ts`) and exposes a cookie-based `RequestContext`. That is
|
|
20515
|
+
* a different auth model that would force an interactive login here. The two
|
|
20516
|
+
* coexist by design — apple-utils backs `apple login`; this client backs
|
|
20517
|
+
* non-interactive ASC API-key access.
|
|
20518
|
+
*/
|
|
20519
|
+
var AscApiError = class extends Data.TaggedError("AscApiError") {};
|
|
20520
|
+
var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
|
|
20521
|
+
const API_BASE = "https://api.appstoreconnect.apple.com";
|
|
20522
|
+
const extractErrors = (body) => {
|
|
20523
|
+
if (!isRecord$1(body) || !Array.isArray(body["errors"])) return [];
|
|
20524
|
+
return body["errors"].filter((value) => isRecord$1(value));
|
|
20525
|
+
};
|
|
20526
|
+
const parseApiError = (response, body, raw) => {
|
|
20527
|
+
const [first] = extractErrors(body);
|
|
20528
|
+
return new AscApiError({
|
|
20529
|
+
status: response.status,
|
|
20530
|
+
message: first?.detail ?? first?.title ?? response.statusText,
|
|
20531
|
+
code: first?.code,
|
|
20532
|
+
raw
|
|
20533
|
+
});
|
|
20534
|
+
};
|
|
20535
|
+
const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
|
|
20536
|
+
const response = yield* Effect.tryPromise({
|
|
20537
|
+
try: async () => fetch(`${API_BASE}${path}`, compact({
|
|
20538
|
+
method: init?.method ?? "GET",
|
|
20539
|
+
body: init?.body,
|
|
20540
|
+
headers: {
|
|
20541
|
+
authorization: `Bearer ${jwt}`,
|
|
20542
|
+
"content-type": "application/json",
|
|
20543
|
+
accept: "application/json"
|
|
20544
|
+
}
|
|
20545
|
+
})),
|
|
20546
|
+
catch: (cause) => new AscNetworkError({ cause })
|
|
20547
|
+
});
|
|
20548
|
+
const text = yield* Effect.tryPromise({
|
|
20549
|
+
try: async () => response.text(),
|
|
20550
|
+
catch: (cause) => new AscNetworkError({ cause })
|
|
20551
|
+
});
|
|
20552
|
+
const body = yield* Effect.try({
|
|
20553
|
+
try: () => text.length === 0 ? {} : JSON.parse(text),
|
|
20554
|
+
catch: (cause) => new AscNetworkError({ cause })
|
|
20555
|
+
});
|
|
20556
|
+
if (!response.ok) return yield* parseApiError(response, body, text);
|
|
20557
|
+
return body;
|
|
20558
|
+
});
|
|
20559
|
+
const toAscCertificate = (value) => {
|
|
20560
|
+
if (!isRecord$1(value)) return null;
|
|
20561
|
+
const { id, attributes } = value;
|
|
20562
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20563
|
+
const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
|
|
20564
|
+
if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
|
|
20565
|
+
return {
|
|
20566
|
+
id,
|
|
20567
|
+
serialNumber,
|
|
20568
|
+
certificateType,
|
|
20569
|
+
expirationDate,
|
|
20570
|
+
certificateContent: typeof certificateContent === "string" ? certificateContent : null,
|
|
20571
|
+
displayName: typeof displayName === "string" ? displayName : null
|
|
20572
|
+
};
|
|
20573
|
+
};
|
|
20574
|
+
const toAscBundleId = (value) => {
|
|
20575
|
+
if (!isRecord$1(value)) return null;
|
|
20576
|
+
const { id, attributes } = value;
|
|
20577
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20578
|
+
const { identifier, name } = attributes;
|
|
20579
|
+
if (typeof identifier !== "string" || typeof name !== "string") return null;
|
|
20580
|
+
return {
|
|
20581
|
+
id,
|
|
20582
|
+
identifier,
|
|
20583
|
+
name
|
|
20584
|
+
};
|
|
20585
|
+
};
|
|
20586
|
+
const PROFILE_TYPES = [
|
|
20587
|
+
"IOS_APP_ADHOC",
|
|
20588
|
+
"IOS_APP_DEVELOPMENT",
|
|
20589
|
+
"IOS_APP_STORE",
|
|
20590
|
+
"IOS_APP_INHOUSE"
|
|
20591
|
+
];
|
|
20592
|
+
const asProfileType = (value) => {
|
|
20593
|
+
const match = PROFILE_TYPES.find((entry) => entry === value);
|
|
20594
|
+
return match === void 0 ? null : match;
|
|
20595
|
+
};
|
|
20596
|
+
const toAscProfile = (value) => {
|
|
20597
|
+
if (!isRecord$1(value)) return null;
|
|
20598
|
+
const { id, attributes } = value;
|
|
20599
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20600
|
+
const { name, uuid, expirationDate, profileContent } = attributes;
|
|
20601
|
+
const profileType = asProfileType(attributes["profileType"]);
|
|
20602
|
+
if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
|
|
20603
|
+
return {
|
|
20604
|
+
id,
|
|
20605
|
+
name,
|
|
20606
|
+
uuid,
|
|
20607
|
+
expirationDate,
|
|
20608
|
+
profileContent,
|
|
20609
|
+
profileType
|
|
20610
|
+
};
|
|
20611
|
+
};
|
|
20612
|
+
const toAscDevice = (value) => {
|
|
20613
|
+
if (!isRecord$1(value)) return null;
|
|
20614
|
+
const { id, attributes } = value;
|
|
20615
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20616
|
+
const { udid, name, deviceClass } = attributes;
|
|
20617
|
+
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
20618
|
+
return {
|
|
20619
|
+
id,
|
|
20620
|
+
udid,
|
|
20621
|
+
name,
|
|
20622
|
+
deviceClass: typeof deviceClass === "string" ? deviceClass : null
|
|
20623
|
+
};
|
|
20624
|
+
};
|
|
20625
|
+
const extractList = (body, map) => {
|
|
20626
|
+
if (!isRecord$1(body) || !Array.isArray(body["data"])) return [];
|
|
20627
|
+
return body["data"].map(map).filter((value) => value !== null);
|
|
20628
|
+
};
|
|
20629
|
+
const extractSingle = (body, map) => {
|
|
20630
|
+
if (!isRecord$1(body)) return null;
|
|
20631
|
+
return map(body["data"]);
|
|
20632
|
+
};
|
|
20633
|
+
/**
|
|
20634
|
+
* App Store Connect paginates list responses (default 200/page) and returns the
|
|
20635
|
+
* absolute URL of the next page under `links.next`. Strip the base so it can be
|
|
20636
|
+
* fed back into `fetchRaw`; return null when there is no further page.
|
|
20637
|
+
*/
|
|
20638
|
+
const nextPagePath = (body) => {
|
|
20639
|
+
if (!isRecord$1(body)) return null;
|
|
20640
|
+
const { links } = body;
|
|
20641
|
+
if (!isRecord$1(links) || typeof links["next"] !== "string") return null;
|
|
20642
|
+
const { next } = links;
|
|
20643
|
+
return next.startsWith(API_BASE) ? next.slice(37) : next;
|
|
20644
|
+
};
|
|
20645
|
+
const malformed = (resource) => new AscApiError({
|
|
20646
|
+
status: 500,
|
|
20647
|
+
message: `Malformed ${resource} response`,
|
|
20648
|
+
code: void 0,
|
|
20649
|
+
raw: ""
|
|
20650
|
+
});
|
|
20651
|
+
const withJwt = (credentials, fn) => Effect.gen(function* () {
|
|
20652
|
+
return yield* fn(yield* signAscJwt(credentials));
|
|
20653
|
+
});
|
|
20654
|
+
const listCertificates = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20655
|
+
return extractList(yield* fetchRaw(jwt, `/v1/certificates${params?.certificateType ? `?filter[certificateType]=${params.certificateType}&limit=200` : "?limit=200"}`), toAscCertificate);
|
|
20656
|
+
}));
|
|
20657
|
+
const createCertificate = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20658
|
+
const csrContent = params.csrPem.replaceAll("-----BEGIN CERTIFICATE REQUEST-----", "").replaceAll("-----END CERTIFICATE REQUEST-----", "").replaceAll(/\s+/gu, "");
|
|
20659
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/certificates", {
|
|
20660
|
+
method: "POST",
|
|
20661
|
+
body: JSON.stringify({ data: {
|
|
20662
|
+
type: "certificates",
|
|
20663
|
+
attributes: {
|
|
20664
|
+
csrContent,
|
|
20665
|
+
certificateType: params.certificateType
|
|
20666
|
+
}
|
|
20667
|
+
} })
|
|
20668
|
+
}), toAscCertificate);
|
|
20669
|
+
if (resource === null) return yield* malformed("certificate");
|
|
20670
|
+
return resource;
|
|
20671
|
+
}));
|
|
20672
|
+
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
|
|
20673
|
+
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20674
|
+
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
20675
|
+
}));
|
|
20676
|
+
const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20677
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
|
|
20678
|
+
method: "POST",
|
|
20679
|
+
body: JSON.stringify({ data: {
|
|
20680
|
+
type: "bundleIds",
|
|
20681
|
+
attributes: {
|
|
20682
|
+
identifier: params.identifier,
|
|
20683
|
+
name: params.name,
|
|
20684
|
+
platform: "IOS"
|
|
20685
|
+
}
|
|
20686
|
+
} })
|
|
20687
|
+
}), toAscBundleId);
|
|
20688
|
+
if (resource === null) return yield* malformed("bundleId");
|
|
20689
|
+
return resource;
|
|
20690
|
+
}));
|
|
20691
|
+
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20692
|
+
const devices = [];
|
|
20693
|
+
let path = "/v1/devices?limit=200";
|
|
20694
|
+
while (path !== null) {
|
|
20695
|
+
const body = yield* fetchRaw(jwt, path);
|
|
20696
|
+
devices.push(...extractList(body, toAscDevice));
|
|
20697
|
+
path = nextPagePath(body);
|
|
20698
|
+
}
|
|
20699
|
+
return devices;
|
|
20700
|
+
}));
|
|
20701
|
+
const createDevice = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20702
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/devices", {
|
|
20703
|
+
method: "POST",
|
|
20704
|
+
body: JSON.stringify({ data: {
|
|
20705
|
+
type: "devices",
|
|
20706
|
+
attributes: {
|
|
20707
|
+
name: params.name,
|
|
20708
|
+
udid: params.udid,
|
|
20709
|
+
platform: "IOS"
|
|
20710
|
+
}
|
|
20711
|
+
} })
|
|
20712
|
+
}), toAscDevice);
|
|
20713
|
+
if (resource === null) return yield* malformed("device");
|
|
20714
|
+
return resource;
|
|
20715
|
+
}));
|
|
20716
|
+
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20717
|
+
const relationships = {
|
|
20718
|
+
bundleId: { data: {
|
|
20719
|
+
type: "bundleIds",
|
|
20720
|
+
id: params.bundleIdAscId
|
|
20721
|
+
} },
|
|
20722
|
+
certificates: { data: params.certificateAscIds.map((id) => ({
|
|
20723
|
+
type: "certificates",
|
|
20724
|
+
id
|
|
20725
|
+
})) },
|
|
20726
|
+
...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
|
|
20727
|
+
type: "devices",
|
|
20728
|
+
id
|
|
20729
|
+
})) } } : {}
|
|
20730
|
+
};
|
|
20731
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
|
|
20732
|
+
method: "POST",
|
|
20733
|
+
body: JSON.stringify({ data: {
|
|
20734
|
+
type: "profiles",
|
|
20735
|
+
attributes: {
|
|
20736
|
+
name: params.profileName,
|
|
20737
|
+
profileType: params.profileType
|
|
20738
|
+
},
|
|
20739
|
+
relationships
|
|
20740
|
+
} })
|
|
20741
|
+
}), toAscProfile);
|
|
20742
|
+
if (resource === null) return yield* malformed("profile");
|
|
20743
|
+
return resource;
|
|
20744
|
+
}));
|
|
20745
|
+
const isCertificateLimitError = (error) => {
|
|
20746
|
+
if (error._tag !== "AscApiError") return false;
|
|
20747
|
+
return /already have a current.*certificate|pending certificate request/iu.test(error.message);
|
|
20748
|
+
};
|
|
20749
|
+
|
|
20750
|
+
//#endregion
|
|
20751
|
+
//#region src/lib/apple-asc-testflight.ts
|
|
20752
|
+
/**
|
|
20753
|
+
* App Store Connect TestFlight operations layered on the ASC API-key client
|
|
20754
|
+
* ({@link ./apple-asc-client}). Used by the iOS submit flow to configure a build
|
|
20755
|
+
* *after* `altool` uploads it: set the "What to Test" text and assign the build
|
|
20756
|
+
* to internal beta groups — matching `eas submit`'s post-upload behaviour.
|
|
20757
|
+
*/
|
|
20758
|
+
const toAscApp = (value) => {
|
|
20759
|
+
if (!isRecord$1(value)) return null;
|
|
20760
|
+
const { id, attributes } = value;
|
|
20761
|
+
if (typeof id !== "string") return null;
|
|
20762
|
+
const attrs = isRecord$1(attributes) ? attributes : {};
|
|
20763
|
+
return {
|
|
20764
|
+
id,
|
|
20765
|
+
bundleId: typeof attrs["bundleId"] === "string" ? attrs["bundleId"] : null,
|
|
20766
|
+
name: typeof attrs["name"] === "string" ? attrs["name"] : null
|
|
20767
|
+
};
|
|
20768
|
+
};
|
|
20769
|
+
const toAscBuild = (value) => {
|
|
20770
|
+
if (!isRecord$1(value)) return null;
|
|
20771
|
+
const { id, attributes } = value;
|
|
20772
|
+
if (typeof id !== "string") return null;
|
|
20773
|
+
const attrs = isRecord$1(attributes) ? attributes : {};
|
|
20774
|
+
return {
|
|
20775
|
+
id,
|
|
20776
|
+
version: typeof attrs["version"] === "string" ? attrs["version"] : null,
|
|
20777
|
+
uploadedDate: typeof attrs["uploadedDate"] === "string" ? attrs["uploadedDate"] : null,
|
|
20778
|
+
processingState: typeof attrs["processingState"] === "string" ? attrs["processingState"] : null
|
|
20779
|
+
};
|
|
20780
|
+
};
|
|
20781
|
+
const toAscBetaGroup = (value) => {
|
|
20782
|
+
if (!isRecord$1(value)) return null;
|
|
20783
|
+
const { id, attributes } = value;
|
|
20784
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20785
|
+
const { name, isInternalGroup } = attributes;
|
|
20786
|
+
if (typeof name !== "string") return null;
|
|
20787
|
+
return {
|
|
20788
|
+
id,
|
|
20789
|
+
name,
|
|
20790
|
+
isInternal: isInternalGroup === true
|
|
20791
|
+
};
|
|
20792
|
+
};
|
|
20793
|
+
const toAscBetaBuildLocalization = (value) => {
|
|
20794
|
+
if (!isRecord$1(value)) return null;
|
|
20795
|
+
const { id, attributes } = value;
|
|
20796
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20797
|
+
const { locale, whatsNew } = attributes;
|
|
20798
|
+
if (typeof locale !== "string") return null;
|
|
20799
|
+
return {
|
|
20800
|
+
id,
|
|
20801
|
+
locale,
|
|
20802
|
+
whatsNew: typeof whatsNew === "string" ? whatsNew : null
|
|
20803
|
+
};
|
|
20804
|
+
};
|
|
20805
|
+
/** Classify a raw `processingState`. Unknown/absent states stay `processing`
|
|
20806
|
+
* so the poller keeps waiting rather than failing early. */
|
|
20807
|
+
const classifyProcessingState = (state) => {
|
|
20808
|
+
if (state === "VALID") return "valid";
|
|
20809
|
+
if (state === "FAILED" || state === "INVALID") return "failed";
|
|
20810
|
+
return "processing";
|
|
20811
|
+
};
|
|
20812
|
+
/**
|
|
20813
|
+
* Identify the build produced by *our* upload. `listRecentBuilds` returns builds
|
|
20814
|
+
* newest-first; the freshly-uploaded build is the newest one whose id differs
|
|
20815
|
+
* from the baseline captured before upload. Comparing ids (not timestamps) avoids
|
|
20816
|
+
* both clock-skew misses and accidentally matching a pre-existing build.
|
|
20817
|
+
*/
|
|
20818
|
+
const pickNewBuild = (builds, baselineLatestBuildId) => {
|
|
20819
|
+
const [newest] = builds;
|
|
20820
|
+
if (newest === void 0 || newest.id === baselineLatestBuildId) return null;
|
|
20821
|
+
return newest;
|
|
20822
|
+
};
|
|
20823
|
+
const matchBetaGroupsByName = (groups, names) => {
|
|
20824
|
+
const byName = new Map(groups.map((group) => [group.name, group]));
|
|
20825
|
+
const matched = [];
|
|
20826
|
+
const missing = [];
|
|
20827
|
+
for (const name of names) {
|
|
20828
|
+
const group = byName.get(name);
|
|
20829
|
+
if (group === void 0) missing.push(name);
|
|
20830
|
+
else matched.push(group);
|
|
20831
|
+
}
|
|
20832
|
+
return {
|
|
20833
|
+
matched,
|
|
20834
|
+
missing
|
|
20835
|
+
};
|
|
20836
|
+
};
|
|
20837
|
+
/** Resolve the ASC app record for a bundle identifier, or null when none exists. */
|
|
20838
|
+
const getAppByBundleId = (credentials, bundleId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20839
|
+
const [first] = extractList(yield* fetchRaw(jwt, `/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}&limit=1`), toAscApp);
|
|
20840
|
+
return first === void 0 ? null : first;
|
|
20841
|
+
}));
|
|
20842
|
+
/** Builds for an app, newest upload first. */
|
|
20843
|
+
const listRecentBuilds = (credentials, appId, limit = 20) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20844
|
+
return extractList(yield* fetchRaw(jwt, `/v1/builds?filter[app]=${encodeURIComponent(appId)}&sort=-uploadedDate&limit=${String(limit)}`), toAscBuild);
|
|
20845
|
+
}));
|
|
20846
|
+
const listBetaGroups = (credentials, appId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20847
|
+
const groups = [];
|
|
20848
|
+
let path = `/v1/betaGroups?filter[app]=${encodeURIComponent(appId)}&limit=200`;
|
|
20849
|
+
while (path !== null) {
|
|
20850
|
+
const body = yield* fetchRaw(jwt, path);
|
|
20851
|
+
groups.push(...extractList(body, toAscBetaGroup));
|
|
20852
|
+
path = nextPagePath(body);
|
|
20853
|
+
}
|
|
20854
|
+
return groups;
|
|
20855
|
+
}));
|
|
20856
|
+
const listBuildBetaLocalizations = (credentials, buildId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20857
|
+
return extractList(yield* fetchRaw(jwt, `/v1/builds/${encodeURIComponent(buildId)}/betaBuildLocalizations?limit=200`), toAscBetaBuildLocalization);
|
|
20858
|
+
}));
|
|
20859
|
+
const createBetaBuildLocalization = (credentials, params) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, "/v1/betaBuildLocalizations", {
|
|
20860
|
+
method: "POST",
|
|
20861
|
+
body: JSON.stringify({ data: {
|
|
20862
|
+
type: "betaBuildLocalizations",
|
|
20863
|
+
attributes: {
|
|
20864
|
+
locale: params.locale,
|
|
20865
|
+
whatsNew: params.whatsNew
|
|
20866
|
+
},
|
|
20867
|
+
relationships: { build: { data: {
|
|
20868
|
+
type: "builds",
|
|
20869
|
+
id: params.buildId
|
|
20870
|
+
} } }
|
|
20871
|
+
} })
|
|
20872
|
+
})));
|
|
20873
|
+
const updateBetaBuildLocalization = (credentials, params) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/betaBuildLocalizations/${encodeURIComponent(params.id)}`, {
|
|
20874
|
+
method: "PATCH",
|
|
20875
|
+
body: JSON.stringify({ data: {
|
|
20876
|
+
type: "betaBuildLocalizations",
|
|
20877
|
+
id: params.id,
|
|
20878
|
+
attributes: { whatsNew: params.whatsNew }
|
|
20879
|
+
} })
|
|
20880
|
+
})));
|
|
20881
|
+
const addBuildToBetaGroups = (credentials, buildId, groupIds) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/builds/${encodeURIComponent(buildId)}/relationships/betaGroups`, {
|
|
20882
|
+
method: "POST",
|
|
20883
|
+
body: JSON.stringify({ data: groupIds.map((id) => ({
|
|
20884
|
+
type: "betaGroups",
|
|
20885
|
+
id
|
|
20886
|
+
})) })
|
|
20887
|
+
})));
|
|
20888
|
+
|
|
20889
|
+
//#endregion
|
|
20890
|
+
//#region src/application/ios-testflight-config.ts
|
|
20891
|
+
/**
|
|
20892
|
+
* Post-upload TestFlight configuration for iOS submissions. After `altool`
|
|
20893
|
+
* uploads the `.ipa`, App Store Connect spends several minutes *processing* the
|
|
20894
|
+
* binary before it can be configured. This module waits for that processing to
|
|
20895
|
+
* finish, then sets the build's "What to Test" text and assigns it to internal
|
|
20896
|
+
* TestFlight groups — the same follow-up `eas submit` performs server-side.
|
|
20897
|
+
*
|
|
20898
|
+
* Auth reuses the ASC **API key** already decrypted for the upload (no second
|
|
20899
|
+
* credential prompt). Failures surface as {@link TestFlightConfigError} so the
|
|
20900
|
+
* caller can mark the submission ERRORED with a precise reason.
|
|
20901
|
+
*/
|
|
20902
|
+
var TestFlightConfigError = class extends Data.TaggedError("TestFlightConfigError") {};
|
|
20903
|
+
const DEFAULT_POLL_TIMEOUT_MS = 15 * 6e4;
|
|
20904
|
+
const DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
20905
|
+
const DEFAULT_LOCALE = "en-US";
|
|
20906
|
+
const ascErrorMessage$1 = (error) => {
|
|
20907
|
+
if (error._tag === "AscApiError") return `App Store Connect API error ${String(error.status)}: ${error.message}`;
|
|
20908
|
+
if (error._tag === "AscNetworkError") return `App Store Connect network error: ${String(error.cause)}`;
|
|
20909
|
+
return `App Store Connect auth error: ${String(error.cause)}`;
|
|
20910
|
+
};
|
|
20911
|
+
const wrapAsc = (code) => (error) => new TestFlightConfigError({
|
|
20912
|
+
code,
|
|
20913
|
+
message: ascErrorMessage$1(error)
|
|
20914
|
+
});
|
|
20915
|
+
/**
|
|
20916
|
+
* Resolve the ASC app id (preferring the explicit `ascAppId`) and snapshot the
|
|
20917
|
+
* latest existing build. Run this *before* `altool` so the freshly-uploaded
|
|
20918
|
+
* build can be distinguished from prior ones.
|
|
20919
|
+
*/
|
|
20920
|
+
const captureTestFlightContext = (params) => Effect.gen(function* () {
|
|
20921
|
+
const appId = params.ascAppId ?? (yield* Effect.gen(function* () {
|
|
20922
|
+
const app = yield* getAppByBundleId(params.credentials, params.bundleIdentifier).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_APP_LOOKUP_FAILED")));
|
|
20923
|
+
if (app === null) return yield* new TestFlightConfigError({
|
|
20924
|
+
code: "TESTFLIGHT_APP_NOT_FOUND",
|
|
20925
|
+
message: `No App Store Connect app found for bundle id ${params.bundleIdentifier}. Set ascAppId in the eas.json submit profile.`
|
|
20926
|
+
});
|
|
20927
|
+
return app.id;
|
|
20928
|
+
}));
|
|
20929
|
+
return {
|
|
20930
|
+
appId,
|
|
20931
|
+
baselineLatestBuildId: toDbNull((yield* listRecentBuilds(params.credentials, appId, 1).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_BUILDS_FAILED"))))[0]?.id)
|
|
20932
|
+
};
|
|
20933
|
+
});
|
|
20934
|
+
const pollForProcessedBuild = (params) => Effect.gen(function* () {
|
|
20935
|
+
const deadline = Date.now() + params.pollTimeoutMs;
|
|
20936
|
+
const final = yield* Effect.iterate({
|
|
20937
|
+
build: null,
|
|
20938
|
+
attempt: 0
|
|
20939
|
+
}, {
|
|
20940
|
+
while: (state) => state.build === null,
|
|
20941
|
+
body: (state) => Effect.gen(function* () {
|
|
20942
|
+
if (state.attempt > 0) yield* Effect.sleep(Duration.millis(params.pollIntervalMs));
|
|
20943
|
+
const candidate = pickNewBuild(yield* listRecentBuilds(params.credentials, params.context.appId, 20).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_BUILDS_FAILED"))), params.context.baselineLatestBuildId);
|
|
20944
|
+
if (candidate !== null) {
|
|
20945
|
+
const processing = classifyProcessingState(candidate.processingState);
|
|
20946
|
+
if (processing === "failed") return yield* new TestFlightConfigError({
|
|
20947
|
+
code: "TESTFLIGHT_BUILD_PROCESSING_FAILED",
|
|
20948
|
+
message: `App Store Connect rejected build ${candidate.version ?? candidate.id} during processing (state ${candidate.processingState ?? "unknown"}).`
|
|
20949
|
+
});
|
|
20950
|
+
if (processing === "valid") return {
|
|
20951
|
+
build: candidate,
|
|
20952
|
+
attempt: state.attempt + 1
|
|
20953
|
+
};
|
|
20954
|
+
}
|
|
20955
|
+
if (Date.now() > deadline) return yield* new TestFlightConfigError({
|
|
20956
|
+
code: "TESTFLIGHT_BUILD_PROCESSING_TIMEOUT",
|
|
20957
|
+
message: `Timed out after ${String(Math.round(params.pollTimeoutMs / 6e4))} min waiting for the uploaded build to finish processing on App Store Connect. The binary uploaded successfully — re-run the TestFlight configuration later.`
|
|
20958
|
+
});
|
|
20959
|
+
yield* printHuman(candidate === null ? "Waiting for the uploaded build to appear on App Store Connect..." : "Build is processing on App Store Connect...");
|
|
20960
|
+
return {
|
|
20961
|
+
build: null,
|
|
20962
|
+
attempt: state.attempt + 1
|
|
20963
|
+
};
|
|
20964
|
+
})
|
|
20965
|
+
});
|
|
20966
|
+
if (final.build === null) return yield* new TestFlightConfigError({
|
|
20967
|
+
code: "TESTFLIGHT_BUILD_NOT_FOUND",
|
|
20968
|
+
message: "Could not locate the uploaded build on App Store Connect."
|
|
20969
|
+
});
|
|
20970
|
+
return final.build;
|
|
20971
|
+
});
|
|
20972
|
+
const applyWhatToTest = (params) => Effect.gen(function* () {
|
|
20973
|
+
const existing = (yield* listBuildBetaLocalizations(params.credentials, params.buildId).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_LOCALIZATIONS_FAILED")))).find((loc) => loc.locale === params.locale);
|
|
20974
|
+
yield* (existing === void 0 ? createBetaBuildLocalization(params.credentials, {
|
|
20975
|
+
buildId: params.buildId,
|
|
20976
|
+
locale: params.locale,
|
|
20977
|
+
whatsNew: params.whatToTest
|
|
20978
|
+
}) : updateBetaBuildLocalization(params.credentials, {
|
|
20979
|
+
id: existing.id,
|
|
20980
|
+
whatsNew: params.whatToTest
|
|
20981
|
+
})).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_SET_WHAT_TO_TEST_FAILED")));
|
|
20982
|
+
});
|
|
20983
|
+
const applyGroups = (params) => Effect.gen(function* () {
|
|
20984
|
+
const allGroups = yield* listBetaGroups(params.credentials, params.appId).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_GROUPS_FAILED")));
|
|
20985
|
+
const { matched, missing } = matchBetaGroupsByName(allGroups, params.groups);
|
|
20986
|
+
if (missing.length > 0) {
|
|
20987
|
+
const available = allGroups.map((group) => group.name).join(", ") || "(none)";
|
|
20988
|
+
return yield* new TestFlightConfigError({
|
|
20989
|
+
code: "TESTFLIGHT_GROUP_NOT_FOUND",
|
|
20990
|
+
message: `TestFlight group(s) not found: ${missing.join(", ")}. Available groups: ${available}.`
|
|
20991
|
+
});
|
|
20992
|
+
}
|
|
20993
|
+
yield* addBuildToBetaGroups(params.credentials, params.buildId, matched.map((group) => group.id)).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_ADD_TO_GROUPS_FAILED")));
|
|
20994
|
+
});
|
|
20995
|
+
/** Whether a profile has any TestFlight config that warrants the processing wait. */
|
|
20996
|
+
const needsTestFlightConfig = (params) => params.whatToTest !== void 0 || params.groups.length > 0;
|
|
20997
|
+
const applyTestFlightConfig = (inputs) => Effect.gen(function* () {
|
|
20998
|
+
yield* printHuman("Configuring TestFlight (waiting for build processing)...");
|
|
20999
|
+
const build = yield* pollForProcessedBuild({
|
|
21000
|
+
credentials: inputs.credentials,
|
|
21001
|
+
context: inputs.context,
|
|
21002
|
+
pollTimeoutMs: inputs.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
|
|
21003
|
+
pollIntervalMs: inputs.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
|
|
21004
|
+
});
|
|
21005
|
+
if (inputs.whatToTest !== void 0) {
|
|
21006
|
+
yield* applyWhatToTest({
|
|
21007
|
+
credentials: inputs.credentials,
|
|
21008
|
+
buildId: build.id,
|
|
21009
|
+
locale: inputs.language ?? DEFAULT_LOCALE,
|
|
21010
|
+
whatToTest: inputs.whatToTest
|
|
21011
|
+
});
|
|
21012
|
+
yield* printHuman(`Set "What to Test" on build ${build.version ?? build.id}.`);
|
|
21013
|
+
}
|
|
21014
|
+
if (inputs.groups.length > 0) {
|
|
21015
|
+
yield* applyGroups({
|
|
21016
|
+
credentials: inputs.credentials,
|
|
21017
|
+
appId: inputs.context.appId,
|
|
21018
|
+
buildId: build.id,
|
|
21019
|
+
groups: inputs.groups
|
|
21020
|
+
});
|
|
21021
|
+
yield* printHuman(`Assigned build to TestFlight group(s): ${inputs.groups.join(", ")}.`);
|
|
21022
|
+
}
|
|
21023
|
+
return {
|
|
21024
|
+
buildId: build.id,
|
|
21025
|
+
buildVersion: build.version
|
|
21026
|
+
};
|
|
21027
|
+
});
|
|
21028
|
+
|
|
20168
21029
|
//#endregion
|
|
20169
21030
|
//#region src/application/submit-flow.ts
|
|
20170
21031
|
const execFileAsync = promisify(execFile);
|
|
@@ -20231,65 +21092,15 @@ const pollSubmissionUntilTerminal = (api, submissionId, pollIntervalMs = 5e3) =>
|
|
|
20231
21092
|
code: "SUBMISSION_POLL_NO_RESULT",
|
|
20232
21093
|
message: "Polling completed without producing a submission"
|
|
20233
21094
|
})) : Effect.succeed(final)));
|
|
20234
|
-
const
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
keyId: creds.keyId,
|
|
20244
|
-
issuerId: creds.issuerId
|
|
20245
|
-
};
|
|
20246
|
-
});
|
|
20247
|
-
const runIosAltoolUpload = (inputs) => Effect.gen(function* () {
|
|
20248
|
-
const creds = yield* writeAscApiKeyP8(inputs.api, inputs.ascApiKeyId);
|
|
20249
|
-
const apiKeyDir = path.dirname(creds.p8Path);
|
|
20250
|
-
yield* inputs.api.submissions.updateStatus({
|
|
20251
|
-
path: { id: inputs.submissionId },
|
|
20252
|
-
payload: { status: "IN_PROGRESS" }
|
|
20253
|
-
}).pipe(Effect.mapError(() => new CliSubmitError({
|
|
20254
|
-
code: "SUBMISSION_PATCH_FAILED",
|
|
20255
|
-
message: "Failed to PATCH submission status to IN_PROGRESS"
|
|
20256
|
-
})));
|
|
20257
|
-
const result = yield* runAltool([
|
|
20258
|
-
"--upload-app",
|
|
20259
|
-
"--type",
|
|
20260
|
-
"ios",
|
|
20261
|
-
"--apiKey",
|
|
20262
|
-
creds.keyId,
|
|
20263
|
-
"--apiIssuer",
|
|
20264
|
-
creds.issuerId,
|
|
20265
|
-
"--apiKeyDir",
|
|
20266
|
-
apiKeyDir,
|
|
20267
|
-
"--file",
|
|
20268
|
-
inputs.ipaPath,
|
|
20269
|
-
"--output-format",
|
|
20270
|
-
"xml"
|
|
20271
|
-
]);
|
|
20272
|
-
const terminalStatus = result.exitCode === 0 ? "FINISHED" : "ERRORED";
|
|
20273
|
-
const errorMessage = result.exitCode === 0 ? null : `xcrun altool exited ${String(result.exitCode)}: ${result.stderr}`;
|
|
20274
|
-
yield* inputs.api.submissions.updateStatus({
|
|
20275
|
-
path: { id: inputs.submissionId },
|
|
20276
|
-
payload: {
|
|
20277
|
-
status: terminalStatus,
|
|
20278
|
-
...errorMessage ? {
|
|
20279
|
-
errorCode: "SUBMISSION_SERVICE_IOS_ALTOOL_FAILED",
|
|
20280
|
-
errorMessage
|
|
20281
|
-
} : {}
|
|
20282
|
-
}
|
|
20283
|
-
}).pipe(Effect.mapError(() => new CliSubmitError({
|
|
20284
|
-
code: "SUBMISSION_PATCH_FAILED",
|
|
20285
|
-
message: "Failed to PATCH submission terminal status"
|
|
20286
|
-
})));
|
|
20287
|
-
return {
|
|
20288
|
-
status: terminalStatus,
|
|
20289
|
-
stdout: result.stdout,
|
|
20290
|
-
stderr: result.stderr
|
|
20291
|
-
};
|
|
20292
|
-
});
|
|
21095
|
+
const patchSubmissionStatus = (api, submissionId, payload) => api.submissions.updateStatus({
|
|
21096
|
+
path: { id: submissionId },
|
|
21097
|
+
payload
|
|
21098
|
+
}).pipe(Effect.mapError(() => new CliSubmitError({
|
|
21099
|
+
code: "SUBMISSION_PATCH_FAILED",
|
|
21100
|
+
message: `Failed to PATCH submission status to ${payload.status}`
|
|
21101
|
+
})));
|
|
21102
|
+
/** A local `path` archive may be given as a plain path or a `file://` URL. */
|
|
21103
|
+
const localPathFromArchiveValue = (value) => value.startsWith("file://") ? fileURLToPath(value) : value;
|
|
20293
21104
|
const readLocalFile = (filePath, errorCode, errorMessageFmt) => Effect.tryPromise({
|
|
20294
21105
|
try: async () => readFile(filePath),
|
|
20295
21106
|
catch: (cause) => new CliSubmitError({
|
|
@@ -20310,7 +21121,7 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
|
|
|
20310
21121
|
},
|
|
20311
21122
|
catch: (cause) => new CliSubmitError({
|
|
20312
21123
|
code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
|
|
20313
|
-
message: `Failed to download
|
|
21124
|
+
message: `Failed to download archive from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
20314
21125
|
})
|
|
20315
21126
|
});
|
|
20316
21127
|
if (!result.ok || result.bytes === null) return yield* new CliSubmitError({
|
|
@@ -20319,7 +21130,153 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
|
|
|
20319
21130
|
});
|
|
20320
21131
|
return result.bytes;
|
|
20321
21132
|
});
|
|
20322
|
-
const readArchiveBytes = (archive) => archive.source === "path" ? Effect.map(readLocalFile(archive.value, "SUBMISSION_ARCHIVE_READ_FAILED", (cause) => `Failed to read
|
|
21133
|
+
const readArchiveBytes = (archive) => archive.source === "path" ? Effect.map(readLocalFile(localPathFromArchiveValue(archive.value), "SUBMISSION_ARCHIVE_READ_FAILED", (cause) => `Failed to read archive at ${archive.value}: ${cause instanceof Error ? cause.message : String(cause)}`), (buf) => new Uint8Array(buf)) : fetchArchiveOverHttp(archive.value);
|
|
21134
|
+
const downloadArchiveToTempFile = (url, extension) => Effect.gen(function* () {
|
|
21135
|
+
const bytes = yield* fetchArchiveOverHttp(url);
|
|
21136
|
+
const target = path.join(tmpdir(), `better-update-submit-${crypto.randomUUID()}${extension}`);
|
|
21137
|
+
yield* Effect.tryPromise({
|
|
21138
|
+
try: async () => writeFile(target, bytes),
|
|
21139
|
+
catch: (cause) => new CliSubmitError({
|
|
21140
|
+
code: "SUBMISSION_ARCHIVE_WRITE_FAILED",
|
|
21141
|
+
message: `Failed to stage archive to ${target}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
21142
|
+
})
|
|
21143
|
+
});
|
|
21144
|
+
return target;
|
|
21145
|
+
});
|
|
21146
|
+
/**
|
|
21147
|
+
* Resolve an archive to a **local file path** on disk, downloading remote
|
|
21148
|
+
* (`build`/`url`) sources first. Store upload tools (`altool`) require a path
|
|
21149
|
+
* they can open — handing them an https URL fails.
|
|
21150
|
+
*/
|
|
21151
|
+
const resolveLocalArchivePath = (archive, extension) => archive.source === "path" ? Effect.succeed(localPathFromArchiveValue(archive.value)) : downloadArchiveToTempFile(archive.value, extension);
|
|
21152
|
+
/** EAS-compatible env var carrying the Apple ID app-specific password. */
|
|
21153
|
+
const APPLE_APP_SPECIFIC_PASSWORD_ENV = "EXPO_APPLE_APP_SPECIFIC_PASSWORD";
|
|
21154
|
+
const hasAppleAppSpecificPassword = () => {
|
|
21155
|
+
const value = process.env[APPLE_APP_SPECIFIC_PASSWORD_ENV];
|
|
21156
|
+
return value !== void 0 && value !== "";
|
|
21157
|
+
};
|
|
21158
|
+
/**
|
|
21159
|
+
* Resolve the upload auth, matching `eas submit` precedence: an app-specific
|
|
21160
|
+
* password (env var + `appleId`) wins when usable; otherwise fall back to the
|
|
21161
|
+
* ASC API key. Returns null when neither is configured.
|
|
21162
|
+
*/
|
|
21163
|
+
const resolveIosUploadAuth = (params) => {
|
|
21164
|
+
if (params.hasAppSpecificPassword && params.appleId !== void 0) return {
|
|
21165
|
+
kind: "app-specific-password",
|
|
21166
|
+
appleId: params.appleId
|
|
21167
|
+
};
|
|
21168
|
+
if (params.ascApiKeyId !== void 0) return {
|
|
21169
|
+
kind: "asc-api-key",
|
|
21170
|
+
ascApiKeyId: params.ascApiKeyId
|
|
21171
|
+
};
|
|
21172
|
+
return null;
|
|
21173
|
+
};
|
|
21174
|
+
const resolveAscCredentials = (api, ascApiKeyId) => fetchAscCredentials(api, ascApiKeyId).pipe(Effect.mapError(() => new CliSubmitError({
|
|
21175
|
+
code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
|
|
21176
|
+
message: `Failed to fetch or decrypt ASC API key ${ascApiKeyId}`
|
|
21177
|
+
})));
|
|
21178
|
+
/** `altool` reads the API key from `--apiKeyDir`; write the decrypted `.p8` there. */
|
|
21179
|
+
const writeP8ForAltool = (credentials) => Effect.gen(function* () {
|
|
21180
|
+
const target = path.join(tmpdir(), `better-update-submit-AuthKey_${credentials.keyId}.p8`);
|
|
21181
|
+
yield* Effect.promise(async () => writeFile(target, credentials.p8Pem, "utf8"));
|
|
21182
|
+
return target;
|
|
21183
|
+
});
|
|
21184
|
+
const baseAltoolArgs = (ipaPath) => [
|
|
21185
|
+
"--upload-app",
|
|
21186
|
+
"--type",
|
|
21187
|
+
"ios",
|
|
21188
|
+
"--file",
|
|
21189
|
+
ipaPath,
|
|
21190
|
+
"--output-format",
|
|
21191
|
+
"xml"
|
|
21192
|
+
];
|
|
21193
|
+
/** Build `altool` args for the chosen auth. The app-specific password is passed
|
|
21194
|
+
* as `@env:` so it never enters argv; `altool` reads it from the inherited env. */
|
|
21195
|
+
const buildAltoolArgs = (params) => Effect.gen(function* () {
|
|
21196
|
+
if (params.auth.kind === "app-specific-password") return [
|
|
21197
|
+
...baseAltoolArgs(params.ipaPath),
|
|
21198
|
+
"--username",
|
|
21199
|
+
params.auth.appleId,
|
|
21200
|
+
"--password",
|
|
21201
|
+
`@env:${APPLE_APP_SPECIFIC_PASSWORD_ENV}`
|
|
21202
|
+
];
|
|
21203
|
+
if (params.ascCredentials === null) return yield* new CliSubmitError({
|
|
21204
|
+
code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
|
|
21205
|
+
message: "ASC API key is required for an asc-api-key upload but was not resolved."
|
|
21206
|
+
});
|
|
21207
|
+
const p8Path = yield* writeP8ForAltool(params.ascCredentials);
|
|
21208
|
+
return [
|
|
21209
|
+
...baseAltoolArgs(params.ipaPath),
|
|
21210
|
+
"--apiKey",
|
|
21211
|
+
params.ascCredentials.keyId,
|
|
21212
|
+
"--apiIssuer",
|
|
21213
|
+
params.ascCredentials.issuerId,
|
|
21214
|
+
"--apiKeyDir",
|
|
21215
|
+
path.dirname(p8Path)
|
|
21216
|
+
];
|
|
21217
|
+
});
|
|
21218
|
+
const runIosSubmit = (inputs) => Effect.gen(function* () {
|
|
21219
|
+
const wantsConfig = needsTestFlightConfig({
|
|
21220
|
+
whatToTest: inputs.config.whatToTest,
|
|
21221
|
+
groups: inputs.config.groups
|
|
21222
|
+
});
|
|
21223
|
+
const credsKeyId = inputs.auth.kind === "asc-api-key" ? inputs.auth.ascApiKeyId : inputs.ascApiKeyId;
|
|
21224
|
+
const ascCredentials = (inputs.auth.kind === "asc-api-key" || wantsConfig) && credsKeyId !== void 0 ? yield* resolveAscCredentials(inputs.api, credsKeyId) : null;
|
|
21225
|
+
const ipaPath = yield* resolveLocalArchivePath(inputs.archive, ".ipa");
|
|
21226
|
+
let tfContext = null;
|
|
21227
|
+
if (wantsConfig && ascCredentials !== null) tfContext = yield* captureTestFlightContext({
|
|
21228
|
+
credentials: ascCredentials,
|
|
21229
|
+
ascAppId: inputs.config.ascAppId,
|
|
21230
|
+
bundleIdentifier: inputs.config.bundleIdentifier
|
|
21231
|
+
}).pipe(Effect.mapError((error) => new CliSubmitError({
|
|
21232
|
+
code: error.code,
|
|
21233
|
+
message: error.message
|
|
21234
|
+
})));
|
|
21235
|
+
else if (wantsConfig) yield* printHuman("Note: \"What to Test\" and TestFlight groups require an ASC API key (ascApiKeyId) — skipping that step for the app-specific-password upload.");
|
|
21236
|
+
const altoolArgs = yield* buildAltoolArgs({
|
|
21237
|
+
auth: inputs.auth,
|
|
21238
|
+
ascCredentials,
|
|
21239
|
+
ipaPath
|
|
21240
|
+
});
|
|
21241
|
+
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "IN_PROGRESS" });
|
|
21242
|
+
const result = yield* runAltool(altoolArgs);
|
|
21243
|
+
if (result.exitCode !== 0) {
|
|
21244
|
+
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
|
|
21245
|
+
status: "ERRORED",
|
|
21246
|
+
errorCode: "SUBMISSION_SERVICE_IOS_ALTOOL_FAILED",
|
|
21247
|
+
errorMessage: `xcrun altool exited ${String(result.exitCode)}: ${result.stderr}`
|
|
21248
|
+
});
|
|
21249
|
+
return { status: "ERRORED" };
|
|
21250
|
+
}
|
|
21251
|
+
yield* printHuman("altool upload complete.");
|
|
21252
|
+
if (tfContext !== null && ascCredentials !== null) yield* applyTestFlightConfig({
|
|
21253
|
+
credentials: ascCredentials,
|
|
21254
|
+
context: tfContext,
|
|
21255
|
+
language: inputs.config.language,
|
|
21256
|
+
whatToTest: inputs.config.whatToTest,
|
|
21257
|
+
groups: inputs.config.groups
|
|
21258
|
+
}).pipe(Effect.catchTag("TestFlightConfigError", (configError) => Effect.gen(function* () {
|
|
21259
|
+
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
|
|
21260
|
+
status: "ERRORED",
|
|
21261
|
+
errorCode: configError.code,
|
|
21262
|
+
errorMessage: configError.message
|
|
21263
|
+
});
|
|
21264
|
+
return yield* new CliSubmitError({
|
|
21265
|
+
code: configError.code,
|
|
21266
|
+
message: configError.message
|
|
21267
|
+
});
|
|
21268
|
+
})));
|
|
21269
|
+
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
|
|
21270
|
+
return { status: "FINISHED" };
|
|
21271
|
+
});
|
|
21272
|
+
|
|
21273
|
+
//#endregion
|
|
21274
|
+
//#region src/application/android-play-submit.ts
|
|
21275
|
+
/**
|
|
21276
|
+
* Client-side Google Play submission: decrypt the service account key, then run
|
|
21277
|
+
* the Play Developer API edit pipeline (insert → upload bundle → assign track →
|
|
21278
|
+
* commit) — the same steps `eas submit` performs server-side.
|
|
21279
|
+
*/
|
|
20323
21280
|
const fetchServiceAccountKeyById = (api, id) => Effect.gen(function* () {
|
|
20324
21281
|
const data = yield* api.googleServiceAccountKeys.download({ path: { id } }).pipe(Effect.mapError(() => new CliSubmitError({
|
|
20325
21282
|
code: "SUBMISSION_ANDROID_SA_KEY_FETCH_FAILED",
|
|
@@ -20342,25 +21299,35 @@ const fetchServiceAccountKeyById = (api, id) => Effect.gen(function* () {
|
|
|
20342
21299
|
});
|
|
20343
21300
|
return json;
|
|
20344
21301
|
});
|
|
21302
|
+
const readServiceAccountFile = (filePath) => Effect.tryPromise({
|
|
21303
|
+
try: async () => new TextDecoder().decode(await readFile(filePath)),
|
|
21304
|
+
catch: (cause) => new CliSubmitError({
|
|
21305
|
+
code: "SUBMISSION_ANDROID_SA_KEY_LOCAL_READ_FAILED",
|
|
21306
|
+
message: `Failed to read service account JSON at ${filePath}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
21307
|
+
})
|
|
21308
|
+
});
|
|
20345
21309
|
const resolveServiceAccountJson = (params) => {
|
|
20346
21310
|
if (params.serviceAccountKeyId !== void 0) return fetchServiceAccountKeyById(params.api, params.serviceAccountKeyId);
|
|
20347
|
-
if (params.serviceAccountKeyPath !== void 0) return
|
|
21311
|
+
if (params.serviceAccountKeyPath !== void 0) return readServiceAccountFile(params.serviceAccountKeyPath);
|
|
20348
21312
|
return Effect.fail(new CliSubmitError({
|
|
20349
21313
|
code: "SUBMISSION_ANDROID_SA_KEY_MISSING",
|
|
20350
21314
|
message: "Android submission requires a service account key. Pass --service-account-key-id <id>, set serviceAccountKeyId in eas.json submit profile, or set serviceAccountKeyPath to a local JSON file."
|
|
20351
21315
|
}));
|
|
20352
21316
|
};
|
|
20353
|
-
const patchSubmissionStatus = (api, submissionId, payload) => api.submissions.updateStatus({
|
|
20354
|
-
path: { id: submissionId },
|
|
20355
|
-
payload
|
|
20356
|
-
}).pipe(Effect.mapError(() => new CliSubmitError({
|
|
20357
|
-
code: "SUBMISSION_PATCH_FAILED",
|
|
20358
|
-
message: `Failed to PATCH submission status to ${payload.status}`
|
|
20359
|
-
})));
|
|
20360
21317
|
const wrapGooglePlayError = (label) => (cause) => new CliSubmitError({
|
|
20361
21318
|
code: `SUBMISSION_ANDROID_${label}`,
|
|
20362
21319
|
message: cause.message
|
|
20363
21320
|
});
|
|
21321
|
+
/**
|
|
21322
|
+
* EAS/Google Play rule: a staged rollout fraction is required for — and only
|
|
21323
|
+
* valid with — releaseStatus `inProgress`. Returns an error message, or null
|
|
21324
|
+
* when the combination is valid.
|
|
21325
|
+
*/
|
|
21326
|
+
const androidRolloutError = (releaseStatus, rollout) => {
|
|
21327
|
+
if (releaseStatus === "inProgress" && rollout === null) return "rollout is required when releaseStatus is 'inProgress' — set submit.<profile>.android.rollout to a 0–1 fraction.";
|
|
21328
|
+
if (releaseStatus !== "inProgress" && rollout !== null) return `rollout is only allowed when releaseStatus is 'inProgress', not '${releaseStatus}'.`;
|
|
21329
|
+
return null;
|
|
21330
|
+
};
|
|
20364
21331
|
const runGooglePlayPipeline = (params) => Effect.gen(function* () {
|
|
20365
21332
|
const edit = yield* insertEdit({
|
|
20366
21333
|
accessToken: params.accessToken,
|
|
@@ -20395,6 +21362,20 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
20395
21362
|
code: "SUBMISSION_ANDROID_APP_ID_MISSING",
|
|
20396
21363
|
message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
|
|
20397
21364
|
});
|
|
21365
|
+
const releaseStatus = inputs.androidProfile.releaseStatus ?? "completed";
|
|
21366
|
+
const rollout = toDbNull(inputs.androidProfile.rollout);
|
|
21367
|
+
const rolloutError = androidRolloutError(releaseStatus, rollout);
|
|
21368
|
+
if (rolloutError !== null) {
|
|
21369
|
+
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
|
|
21370
|
+
status: "ERRORED",
|
|
21371
|
+
errorCode: "SUBMISSION_ANDROID_ROLLOUT_INVALID",
|
|
21372
|
+
errorMessage: rolloutError
|
|
21373
|
+
});
|
|
21374
|
+
return yield* new CliSubmitError({
|
|
21375
|
+
code: "SUBMISSION_ANDROID_ROLLOUT_INVALID",
|
|
21376
|
+
message: rolloutError
|
|
21377
|
+
});
|
|
21378
|
+
}
|
|
20398
21379
|
const serviceAccountJson = yield* resolveServiceAccountJson({
|
|
20399
21380
|
api: inputs.api,
|
|
20400
21381
|
serviceAccountKeyId: inputs.serviceAccountKeyId,
|
|
@@ -20409,9 +21390,9 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
20409
21390
|
applicationId,
|
|
20410
21391
|
aab,
|
|
20411
21392
|
track: inputs.androidProfile.track ?? "internal",
|
|
20412
|
-
releaseStatus
|
|
21393
|
+
releaseStatus,
|
|
20413
21394
|
changesNotSentForReview: inputs.androidProfile.changesNotSentForReview ?? false,
|
|
20414
|
-
rollout
|
|
21395
|
+
rollout
|
|
20415
21396
|
});
|
|
20416
21397
|
}).pipe(Effect.catchTag("CliSubmitError", (engineError) => Effect.gen(function* () {
|
|
20417
21398
|
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
|
|
@@ -20473,13 +21454,45 @@ const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
|
20473
21454
|
})
|
|
20474
21455
|
});
|
|
20475
21456
|
yield* printHuman(`Submission created: ${submission.id} (${submission.status})`);
|
|
20476
|
-
if (input.platform === "ios" &&
|
|
20477
|
-
|
|
20478
|
-
|
|
21457
|
+
if (input.platform === "ios" && iosConfig !== void 0) {
|
|
21458
|
+
const auth = resolveIosUploadAuth({
|
|
21459
|
+
appleId: easProfile.ios?.appleId,
|
|
21460
|
+
ascApiKeyId: easProfile.ios?.ascApiKeyId,
|
|
21461
|
+
hasAppSpecificPassword: hasAppleAppSpecificPassword()
|
|
21462
|
+
});
|
|
21463
|
+
if (auth === null) yield* printHuman("Skipping iOS upload: configure ascApiKeyId or set EXPO_APPLE_APP_SPECIFIC_PASSWORD (+ appleId).");
|
|
21464
|
+
else {
|
|
21465
|
+
yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
|
|
21466
|
+
yield* runIosSubmit({
|
|
21467
|
+
api: input.api,
|
|
21468
|
+
submissionId: submission.id,
|
|
21469
|
+
archive: {
|
|
21470
|
+
source: "build",
|
|
21471
|
+
value: archiveUrl
|
|
21472
|
+
},
|
|
21473
|
+
auth,
|
|
21474
|
+
ascApiKeyId: easProfile.ios?.ascApiKeyId,
|
|
21475
|
+
config: {
|
|
21476
|
+
bundleIdentifier: iosConfig.bundleIdentifier,
|
|
21477
|
+
ascAppId: easProfile.ios?.ascAppId,
|
|
21478
|
+
language: easProfile.ios?.language,
|
|
21479
|
+
whatToTest: input.whatToTest,
|
|
21480
|
+
groups: easProfile.ios?.groups ?? []
|
|
21481
|
+
}
|
|
21482
|
+
});
|
|
21483
|
+
}
|
|
21484
|
+
}
|
|
21485
|
+
if (input.platform === "android" && androidConfig !== void 0 && easProfile.android !== void 0) {
|
|
21486
|
+
yield* printHuman("Uploading bundle to Google Play...");
|
|
21487
|
+
yield* runAndroidGooglePlayUpload({
|
|
20479
21488
|
api: input.api,
|
|
20480
21489
|
submissionId: submission.id,
|
|
20481
|
-
|
|
20482
|
-
|
|
21490
|
+
archive: {
|
|
21491
|
+
source: "build",
|
|
21492
|
+
value: archiveUrl
|
|
21493
|
+
},
|
|
21494
|
+
androidProfile: easProfile.android,
|
|
21495
|
+
serviceAccountKeyId: easProfile.android.serviceAccountKeyId
|
|
20483
21496
|
});
|
|
20484
21497
|
}
|
|
20485
21498
|
yield* printHuman(`Submission final status: ${(yield* pollSubmissionUntilTerminal(input.api, submission.id)).status}`);
|
|
@@ -20650,313 +21663,6 @@ const generateAndroidKeystore = (input) => Command.exitCode(Command.make("keytoo
|
|
|
20650
21663
|
message: `generate android keystore exited with code ${code}`
|
|
20651
21664
|
}))));
|
|
20652
21665
|
|
|
20653
|
-
//#endregion
|
|
20654
|
-
//#region src/lib/apple-pem.ts
|
|
20655
|
-
const PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
|
|
20656
|
-
const PEM_FOOTER = "-----END PRIVATE KEY-----";
|
|
20657
|
-
const pemToPkcs8Der = (pem) => {
|
|
20658
|
-
const normalized = pem.replaceAll("\r\n", "\n").trim();
|
|
20659
|
-
const start = normalized.indexOf(PEM_HEADER);
|
|
20660
|
-
const end = normalized.indexOf(PEM_FOOTER);
|
|
20661
|
-
if (start === -1 || end === -1 || end <= start) return null;
|
|
20662
|
-
const body = normalized.slice(start + 27, end).replaceAll(/\s+/gu, "").trim();
|
|
20663
|
-
if (body.length === 0) return null;
|
|
20664
|
-
try {
|
|
20665
|
-
return fromBase64(body);
|
|
20666
|
-
} catch {
|
|
20667
|
-
return null;
|
|
20668
|
-
}
|
|
20669
|
-
};
|
|
20670
|
-
|
|
20671
|
-
//#endregion
|
|
20672
|
-
//#region src/lib/apple-asc-jwt.ts
|
|
20673
|
-
var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
|
|
20674
|
-
const MAX_JWT_LIFETIME_SECONDS = 1200;
|
|
20675
|
-
const asArrayBuffer = (bytes) => {
|
|
20676
|
-
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
20677
|
-
new Uint8Array(buffer).set(bytes);
|
|
20678
|
-
return buffer;
|
|
20679
|
-
};
|
|
20680
|
-
const signAscJwt = (credentials) => Effect.gen(function* () {
|
|
20681
|
-
const der = pemToPkcs8Der(credentials.p8Pem);
|
|
20682
|
-
if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
|
|
20683
|
-
const header = {
|
|
20684
|
-
alg: "ES256",
|
|
20685
|
-
kid: credentials.keyId,
|
|
20686
|
-
typ: "JWT"
|
|
20687
|
-
};
|
|
20688
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
20689
|
-
const payload = {
|
|
20690
|
-
iss: credentials.issuerId,
|
|
20691
|
-
iat: now,
|
|
20692
|
-
exp: now + MAX_JWT_LIFETIME_SECONDS,
|
|
20693
|
-
aud: "appstoreconnect-v1"
|
|
20694
|
-
};
|
|
20695
|
-
const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
20696
|
-
const key = yield* Effect.tryPromise({
|
|
20697
|
-
try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
|
|
20698
|
-
name: "ECDSA",
|
|
20699
|
-
namedCurve: "P-256"
|
|
20700
|
-
}, false, ["sign"]),
|
|
20701
|
-
catch: (cause) => new AppleAuthError({ cause })
|
|
20702
|
-
});
|
|
20703
|
-
const signature = yield* Effect.tryPromise({
|
|
20704
|
-
try: async () => crypto.subtle.sign({
|
|
20705
|
-
name: "ECDSA",
|
|
20706
|
-
hash: "SHA-256"
|
|
20707
|
-
}, key, new TextEncoder().encode(signingInput)),
|
|
20708
|
-
catch: (cause) => new AppleAuthError({ cause })
|
|
20709
|
-
});
|
|
20710
|
-
return `${signingInput}.${toBase64Url(new Uint8Array(signature))}`;
|
|
20711
|
-
});
|
|
20712
|
-
|
|
20713
|
-
//#endregion
|
|
20714
|
-
//#region src/lib/apple-asc-client.ts
|
|
20715
|
-
/**
|
|
20716
|
-
* App Store Connect REST client authenticated with an ASC **API key** — a JWT
|
|
20717
|
-
* signed from a `.p8` private key (see `apple-asc-jwt.ts`). Credentials are
|
|
20718
|
-
* resolved non-interactively from the server (`fetchAscCredentials`), so this
|
|
20719
|
-
* powers headless flows: build-credential resolution, provisioning-profile
|
|
20720
|
-
* generation, and device sync.
|
|
20721
|
-
*
|
|
20722
|
-
* Intentionally NOT built on `@expo/apple-utils`: that library authenticates
|
|
20723
|
-
* via an interactive Apple-ID **cookie session** (username/password + 2FA, see
|
|
20724
|
-
* `services/apple-auth.ts`) and exposes a cookie-based `RequestContext`. That is
|
|
20725
|
-
* a different auth model that would force an interactive login here. The two
|
|
20726
|
-
* coexist by design — apple-utils backs `apple login`; this client backs
|
|
20727
|
-
* non-interactive ASC API-key access.
|
|
20728
|
-
*/
|
|
20729
|
-
var AscApiError = class extends Data.TaggedError("AscApiError") {};
|
|
20730
|
-
var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
|
|
20731
|
-
const API_BASE = "https://api.appstoreconnect.apple.com";
|
|
20732
|
-
const extractErrors = (body) => {
|
|
20733
|
-
if (!isRecord$1(body) || !Array.isArray(body["errors"])) return [];
|
|
20734
|
-
return body["errors"].filter((value) => isRecord$1(value));
|
|
20735
|
-
};
|
|
20736
|
-
const parseApiError = (response, body, raw) => {
|
|
20737
|
-
const [first] = extractErrors(body);
|
|
20738
|
-
return new AscApiError({
|
|
20739
|
-
status: response.status,
|
|
20740
|
-
message: first?.detail ?? first?.title ?? response.statusText,
|
|
20741
|
-
code: first?.code,
|
|
20742
|
-
raw
|
|
20743
|
-
});
|
|
20744
|
-
};
|
|
20745
|
-
const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
|
|
20746
|
-
const response = yield* Effect.tryPromise({
|
|
20747
|
-
try: async () => fetch(`${API_BASE}${path}`, compact({
|
|
20748
|
-
method: init?.method ?? "GET",
|
|
20749
|
-
body: init?.body,
|
|
20750
|
-
headers: {
|
|
20751
|
-
authorization: `Bearer ${jwt}`,
|
|
20752
|
-
"content-type": "application/json",
|
|
20753
|
-
accept: "application/json"
|
|
20754
|
-
}
|
|
20755
|
-
})),
|
|
20756
|
-
catch: (cause) => new AscNetworkError({ cause })
|
|
20757
|
-
});
|
|
20758
|
-
const text = yield* Effect.tryPromise({
|
|
20759
|
-
try: async () => response.text(),
|
|
20760
|
-
catch: (cause) => new AscNetworkError({ cause })
|
|
20761
|
-
});
|
|
20762
|
-
const body = yield* Effect.try({
|
|
20763
|
-
try: () => text.length === 0 ? {} : JSON.parse(text),
|
|
20764
|
-
catch: (cause) => new AscNetworkError({ cause })
|
|
20765
|
-
});
|
|
20766
|
-
if (!response.ok) return yield* parseApiError(response, body, text);
|
|
20767
|
-
return body;
|
|
20768
|
-
});
|
|
20769
|
-
const toAscCertificate = (value) => {
|
|
20770
|
-
if (!isRecord$1(value)) return null;
|
|
20771
|
-
const { id, attributes } = value;
|
|
20772
|
-
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20773
|
-
const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
|
|
20774
|
-
if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
|
|
20775
|
-
return {
|
|
20776
|
-
id,
|
|
20777
|
-
serialNumber,
|
|
20778
|
-
certificateType,
|
|
20779
|
-
expirationDate,
|
|
20780
|
-
certificateContent: typeof certificateContent === "string" ? certificateContent : null,
|
|
20781
|
-
displayName: typeof displayName === "string" ? displayName : null
|
|
20782
|
-
};
|
|
20783
|
-
};
|
|
20784
|
-
const toAscBundleId = (value) => {
|
|
20785
|
-
if (!isRecord$1(value)) return null;
|
|
20786
|
-
const { id, attributes } = value;
|
|
20787
|
-
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20788
|
-
const { identifier, name } = attributes;
|
|
20789
|
-
if (typeof identifier !== "string" || typeof name !== "string") return null;
|
|
20790
|
-
return {
|
|
20791
|
-
id,
|
|
20792
|
-
identifier,
|
|
20793
|
-
name
|
|
20794
|
-
};
|
|
20795
|
-
};
|
|
20796
|
-
const PROFILE_TYPES = [
|
|
20797
|
-
"IOS_APP_ADHOC",
|
|
20798
|
-
"IOS_APP_DEVELOPMENT",
|
|
20799
|
-
"IOS_APP_STORE",
|
|
20800
|
-
"IOS_APP_INHOUSE"
|
|
20801
|
-
];
|
|
20802
|
-
const asProfileType = (value) => {
|
|
20803
|
-
const match = PROFILE_TYPES.find((entry) => entry === value);
|
|
20804
|
-
return match === void 0 ? null : match;
|
|
20805
|
-
};
|
|
20806
|
-
const toAscProfile = (value) => {
|
|
20807
|
-
if (!isRecord$1(value)) return null;
|
|
20808
|
-
const { id, attributes } = value;
|
|
20809
|
-
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20810
|
-
const { name, uuid, expirationDate, profileContent } = attributes;
|
|
20811
|
-
const profileType = asProfileType(attributes["profileType"]);
|
|
20812
|
-
if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
|
|
20813
|
-
return {
|
|
20814
|
-
id,
|
|
20815
|
-
name,
|
|
20816
|
-
uuid,
|
|
20817
|
-
expirationDate,
|
|
20818
|
-
profileContent,
|
|
20819
|
-
profileType
|
|
20820
|
-
};
|
|
20821
|
-
};
|
|
20822
|
-
const toAscDevice = (value) => {
|
|
20823
|
-
if (!isRecord$1(value)) return null;
|
|
20824
|
-
const { id, attributes } = value;
|
|
20825
|
-
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
20826
|
-
const { udid, name, deviceClass } = attributes;
|
|
20827
|
-
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
20828
|
-
return {
|
|
20829
|
-
id,
|
|
20830
|
-
udid,
|
|
20831
|
-
name,
|
|
20832
|
-
deviceClass: typeof deviceClass === "string" ? deviceClass : null
|
|
20833
|
-
};
|
|
20834
|
-
};
|
|
20835
|
-
const extractList = (body, map) => {
|
|
20836
|
-
if (!isRecord$1(body) || !Array.isArray(body["data"])) return [];
|
|
20837
|
-
return body["data"].map(map).filter((value) => value !== null);
|
|
20838
|
-
};
|
|
20839
|
-
const extractSingle = (body, map) => {
|
|
20840
|
-
if (!isRecord$1(body)) return null;
|
|
20841
|
-
return map(body["data"]);
|
|
20842
|
-
};
|
|
20843
|
-
/**
|
|
20844
|
-
* App Store Connect paginates list responses (default 200/page) and returns the
|
|
20845
|
-
* absolute URL of the next page under `links.next`. Strip the base so it can be
|
|
20846
|
-
* fed back into `fetchRaw`; return null when there is no further page.
|
|
20847
|
-
*/
|
|
20848
|
-
const nextPagePath = (body) => {
|
|
20849
|
-
if (!isRecord$1(body)) return null;
|
|
20850
|
-
const { links } = body;
|
|
20851
|
-
if (!isRecord$1(links) || typeof links["next"] !== "string") return null;
|
|
20852
|
-
const { next } = links;
|
|
20853
|
-
return next.startsWith(API_BASE) ? next.slice(37) : next;
|
|
20854
|
-
};
|
|
20855
|
-
const malformed = (resource) => new AscApiError({
|
|
20856
|
-
status: 500,
|
|
20857
|
-
message: `Malformed ${resource} response`,
|
|
20858
|
-
code: void 0,
|
|
20859
|
-
raw: ""
|
|
20860
|
-
});
|
|
20861
|
-
const withJwt = (credentials, fn) => Effect.gen(function* () {
|
|
20862
|
-
return yield* fn(yield* signAscJwt(credentials));
|
|
20863
|
-
});
|
|
20864
|
-
const listCertificates = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20865
|
-
return extractList(yield* fetchRaw(jwt, `/v1/certificates${params?.certificateType ? `?filter[certificateType]=${params.certificateType}&limit=200` : "?limit=200"}`), toAscCertificate);
|
|
20866
|
-
}));
|
|
20867
|
-
const createCertificate = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20868
|
-
const csrContent = params.csrPem.replaceAll("-----BEGIN CERTIFICATE REQUEST-----", "").replaceAll("-----END CERTIFICATE REQUEST-----", "").replaceAll(/\s+/gu, "");
|
|
20869
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/certificates", {
|
|
20870
|
-
method: "POST",
|
|
20871
|
-
body: JSON.stringify({ data: {
|
|
20872
|
-
type: "certificates",
|
|
20873
|
-
attributes: {
|
|
20874
|
-
csrContent,
|
|
20875
|
-
certificateType: params.certificateType
|
|
20876
|
-
}
|
|
20877
|
-
} })
|
|
20878
|
-
}), toAscCertificate);
|
|
20879
|
-
if (resource === null) return yield* malformed("certificate");
|
|
20880
|
-
return resource;
|
|
20881
|
-
}));
|
|
20882
|
-
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
|
|
20883
|
-
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20884
|
-
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
20885
|
-
}));
|
|
20886
|
-
const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20887
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
|
|
20888
|
-
method: "POST",
|
|
20889
|
-
body: JSON.stringify({ data: {
|
|
20890
|
-
type: "bundleIds",
|
|
20891
|
-
attributes: {
|
|
20892
|
-
identifier: params.identifier,
|
|
20893
|
-
name: params.name,
|
|
20894
|
-
platform: "IOS"
|
|
20895
|
-
}
|
|
20896
|
-
} })
|
|
20897
|
-
}), toAscBundleId);
|
|
20898
|
-
if (resource === null) return yield* malformed("bundleId");
|
|
20899
|
-
return resource;
|
|
20900
|
-
}));
|
|
20901
|
-
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20902
|
-
const devices = [];
|
|
20903
|
-
let path = "/v1/devices?limit=200";
|
|
20904
|
-
while (path !== null) {
|
|
20905
|
-
const body = yield* fetchRaw(jwt, path);
|
|
20906
|
-
devices.push(...extractList(body, toAscDevice));
|
|
20907
|
-
path = nextPagePath(body);
|
|
20908
|
-
}
|
|
20909
|
-
return devices;
|
|
20910
|
-
}));
|
|
20911
|
-
const createDevice = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20912
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/devices", {
|
|
20913
|
-
method: "POST",
|
|
20914
|
-
body: JSON.stringify({ data: {
|
|
20915
|
-
type: "devices",
|
|
20916
|
-
attributes: {
|
|
20917
|
-
name: params.name,
|
|
20918
|
-
udid: params.udid,
|
|
20919
|
-
platform: "IOS"
|
|
20920
|
-
}
|
|
20921
|
-
} })
|
|
20922
|
-
}), toAscDevice);
|
|
20923
|
-
if (resource === null) return yield* malformed("device");
|
|
20924
|
-
return resource;
|
|
20925
|
-
}));
|
|
20926
|
-
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
20927
|
-
const relationships = {
|
|
20928
|
-
bundleId: { data: {
|
|
20929
|
-
type: "bundleIds",
|
|
20930
|
-
id: params.bundleIdAscId
|
|
20931
|
-
} },
|
|
20932
|
-
certificates: { data: params.certificateAscIds.map((id) => ({
|
|
20933
|
-
type: "certificates",
|
|
20934
|
-
id
|
|
20935
|
-
})) },
|
|
20936
|
-
...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
|
|
20937
|
-
type: "devices",
|
|
20938
|
-
id
|
|
20939
|
-
})) } } : {}
|
|
20940
|
-
};
|
|
20941
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
|
|
20942
|
-
method: "POST",
|
|
20943
|
-
body: JSON.stringify({ data: {
|
|
20944
|
-
type: "profiles",
|
|
20945
|
-
attributes: {
|
|
20946
|
-
name: params.profileName,
|
|
20947
|
-
profileType: params.profileType
|
|
20948
|
-
},
|
|
20949
|
-
relationships
|
|
20950
|
-
} })
|
|
20951
|
-
}), toAscProfile);
|
|
20952
|
-
if (resource === null) return yield* malformed("profile");
|
|
20953
|
-
return resource;
|
|
20954
|
-
}));
|
|
20955
|
-
const isCertificateLimitError = (error) => {
|
|
20956
|
-
if (error._tag !== "AscApiError") return false;
|
|
20957
|
-
return /already have a current.*certificate|pending certificate request/iu.test(error.message);
|
|
20958
|
-
};
|
|
20959
|
-
|
|
20960
21666
|
//#endregion
|
|
20961
21667
|
//#region src/lib/apple-cert-to-p12.ts
|
|
20962
21668
|
var CertParseError = class extends Data.TaggedError("CertParseError") {};
|
|
@@ -24210,34 +24916,6 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
24210
24916
|
|
|
24211
24917
|
//#endregion
|
|
24212
24918
|
//#region src/commands/build/configure.ts
|
|
24213
|
-
const DEFAULT_EAS_JSON = {
|
|
24214
|
-
cli: { version: ">= 7.0.0" },
|
|
24215
|
-
build: {
|
|
24216
|
-
development: {
|
|
24217
|
-
developmentClient: true,
|
|
24218
|
-
distribution: "internal",
|
|
24219
|
-
channel: "development",
|
|
24220
|
-
environment: "development",
|
|
24221
|
-
android: { format: "apk" }
|
|
24222
|
-
},
|
|
24223
|
-
preview: {
|
|
24224
|
-
distribution: "internal",
|
|
24225
|
-
channel: "preview",
|
|
24226
|
-
environment: "preview",
|
|
24227
|
-
android: { format: "apk" }
|
|
24228
|
-
},
|
|
24229
|
-
production: {
|
|
24230
|
-
channel: "production",
|
|
24231
|
-
environment: "production",
|
|
24232
|
-
android: { format: "aab" }
|
|
24233
|
-
}
|
|
24234
|
-
}
|
|
24235
|
-
};
|
|
24236
|
-
const DEFAULT_PROFILES = [
|
|
24237
|
-
"development",
|
|
24238
|
-
"preview",
|
|
24239
|
-
"production"
|
|
24240
|
-
];
|
|
24241
24919
|
const writeEasJson = (filePath, value) => Effect.gen(function* () {
|
|
24242
24920
|
yield* (yield* FileSystem.FileSystem).writeFileString(filePath, `${JSON.stringify(value, null, 2)}\n`).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to write eas.json: ${cause.message}` })));
|
|
24243
24921
|
});
|
|
@@ -24253,43 +24931,44 @@ const configureBuildCommand = defineCommand({
|
|
|
24253
24931
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
24254
24932
|
const { allow: interactive } = yield* InteractiveMode;
|
|
24255
24933
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
24256
|
-
const
|
|
24934
|
+
const filePath = easJsonPath(projectRoot);
|
|
24257
24935
|
const fs = yield* FileSystem.FileSystem;
|
|
24258
|
-
|
|
24259
|
-
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
24260
|
-
yield* printHuman(`Wrote eas.json with default profiles to ${easJsonPath}.`);
|
|
24261
|
-
yield* printHumanKeyValue([["Profiles", DEFAULT_PROFILES.join(", ")], ["Path", easJsonPath]]);
|
|
24262
|
-
return {
|
|
24263
|
-
action: "created",
|
|
24264
|
-
path: easJsonPath,
|
|
24265
|
-
profiles: [...DEFAULT_PROFILES]
|
|
24266
|
-
};
|
|
24267
|
-
}
|
|
24936
|
+
const exists = yield* fs.exists(filePath);
|
|
24268
24937
|
if (args.force === true) {
|
|
24269
|
-
if (!(interactive ? yield* promptConfirm(`Overwrite existing eas.json at ${
|
|
24938
|
+
if (!(exists && interactive ? yield* promptConfirm(`Overwrite existing eas.json at ${filePath} with defaults?`) : true)) {
|
|
24270
24939
|
yield* printHuman("Aborted. eas.json was not modified.");
|
|
24271
24940
|
return {
|
|
24272
24941
|
action: "aborted",
|
|
24273
|
-
path:
|
|
24942
|
+
path: filePath
|
|
24274
24943
|
};
|
|
24275
24944
|
}
|
|
24276
|
-
yield* writeEasJson(
|
|
24277
|
-
yield* printHuman(
|
|
24945
|
+
yield* writeEasJson(filePath, DEFAULT_EAS_JSON);
|
|
24946
|
+
yield* printHuman(exists ? "Overwrote eas.json with default profiles." : `Wrote eas.json with default profiles to ${filePath}.`);
|
|
24278
24947
|
return {
|
|
24279
|
-
action: "overwritten",
|
|
24280
|
-
path:
|
|
24281
|
-
profiles: [...
|
|
24948
|
+
action: exists ? "overwritten" : "created",
|
|
24949
|
+
path: filePath,
|
|
24950
|
+
profiles: [...DEFAULT_PROFILE_NAMES]
|
|
24951
|
+
};
|
|
24952
|
+
}
|
|
24953
|
+
if (!exists) {
|
|
24954
|
+
const created = yield* ensureDefaultBuildProfiles(projectRoot);
|
|
24955
|
+
yield* printHuman(`Wrote eas.json with default profiles to ${created.path}.`);
|
|
24956
|
+
yield* printHumanKeyValue([["Profiles", created.added.join(", ")], ["Path", created.path]]);
|
|
24957
|
+
return {
|
|
24958
|
+
action: "created",
|
|
24959
|
+
path: created.path,
|
|
24960
|
+
profiles: created.added
|
|
24282
24961
|
};
|
|
24283
24962
|
}
|
|
24284
|
-
const config = yield* parseEasConfig(yield* fs.readFileString(
|
|
24963
|
+
const config = yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to read eas.json: ${cause.message}` }))));
|
|
24285
24964
|
const existingProfiles = Object.keys(config.build ?? {});
|
|
24286
|
-
const missing =
|
|
24965
|
+
const missing = DEFAULT_PROFILE_NAMES.filter((name) => !existingProfiles.includes(name));
|
|
24287
24966
|
if (missing.length === 0) {
|
|
24288
24967
|
yield* printHuman(`eas.json already defines all default profiles (${existingProfiles.join(", ")}). Nothing to add.`);
|
|
24289
24968
|
yield* printHuman("Pass --force to overwrite with the default template.");
|
|
24290
24969
|
return {
|
|
24291
24970
|
action: "noop",
|
|
24292
|
-
path:
|
|
24971
|
+
path: filePath,
|
|
24293
24972
|
existing: existingProfiles
|
|
24294
24973
|
};
|
|
24295
24974
|
}
|
|
@@ -24297,28 +24976,21 @@ const configureBuildCommand = defineCommand({
|
|
|
24297
24976
|
yield* printHuman("Aborted. eas.json was not modified.");
|
|
24298
24977
|
return {
|
|
24299
24978
|
action: "aborted",
|
|
24300
|
-
path:
|
|
24979
|
+
path: filePath
|
|
24301
24980
|
};
|
|
24302
24981
|
}
|
|
24303
|
-
const
|
|
24304
|
-
yield*
|
|
24305
|
-
build: {
|
|
24306
|
-
...config.build,
|
|
24307
|
-
...additions
|
|
24308
|
-
},
|
|
24309
|
-
...compact({ cli: config.cli })
|
|
24310
|
-
});
|
|
24311
|
-
yield* printHuman(`Added profile(s) to eas.json: ${missing.join(", ")}.`);
|
|
24982
|
+
const result = yield* ensureDefaultBuildProfiles(projectRoot);
|
|
24983
|
+
yield* printHuman(`Added profile(s) to eas.json: ${result.added.join(", ")}.`);
|
|
24312
24984
|
yield* printHumanKeyValue([
|
|
24313
24985
|
["Existing", existingProfiles.join(", ") || "(none)"],
|
|
24314
|
-
["Added",
|
|
24315
|
-
["Path",
|
|
24986
|
+
["Added", result.added.join(", ")],
|
|
24987
|
+
["Path", result.path]
|
|
24316
24988
|
]);
|
|
24317
24989
|
return {
|
|
24318
24990
|
action: "topped-up",
|
|
24319
|
-
path:
|
|
24991
|
+
path: result.path,
|
|
24320
24992
|
existing: existingProfiles,
|
|
24321
|
-
added:
|
|
24993
|
+
added: result.added
|
|
24322
24994
|
};
|
|
24323
24995
|
}), { json: "value" })
|
|
24324
24996
|
});
|
|
@@ -25858,13 +26530,160 @@ const inspectP12 = (params) => Effect.try({
|
|
|
25858
26530
|
catch: (error) => new CredentialValidationError({ message: `Failed to parse P12 certificate: ${error instanceof Error ? error.message : String(error)}` })
|
|
25859
26531
|
});
|
|
25860
26532
|
|
|
26533
|
+
//#endregion
|
|
26534
|
+
//#region src/lib/credentials-pass-type-certificate.ts
|
|
26535
|
+
/**
|
|
26536
|
+
* Manual upload of a Wallet Pass Type ID `.p12` certificate. The Pass Type ID
|
|
26537
|
+
* (`pass.*`) is passed explicitly; serial/validity come from the parsed cert and
|
|
26538
|
+
* the team from its OU (or `--apple-team-identifier`). Only
|
|
26539
|
+
* `{ p12Base64, p12Password }` is sealed.
|
|
26540
|
+
*/
|
|
26541
|
+
const uploadIosPassTypeCertificate = (api, input, bytes) => Effect.gen(function* () {
|
|
26542
|
+
if (input.password === void 0) return yield* new CredentialValidationError({ message: "Missing --password required for the selected credential type." });
|
|
26543
|
+
if (!input.passTypeIdentifier) return yield* new CredentialValidationError({ message: "Missing --pass-type-identifier required for a Pass Type ID certificate." });
|
|
26544
|
+
const info = yield* inspectP12({
|
|
26545
|
+
data: Buffer.from(bytes),
|
|
26546
|
+
password: input.password
|
|
26547
|
+
});
|
|
26548
|
+
const appleTeamIdentifier = info.teamId ?? input.appleTeamIdentifier;
|
|
26549
|
+
if (!appleTeamIdentifier) return yield* new CredentialValidationError({ message: "Could not derive Apple Team ID from the certificate; pass --apple-team-identifier." });
|
|
26550
|
+
if (!info.validFrom || !info.expiresAt) return yield* new CredentialValidationError({ message: "Certificate is missing notBefore/notAfter dates." });
|
|
26551
|
+
const metadata = {
|
|
26552
|
+
passTypeIdentifier: input.passTypeIdentifier,
|
|
26553
|
+
serialNumber: info.serialNumber,
|
|
26554
|
+
appleTeamIdentifier,
|
|
26555
|
+
validFrom: info.validFrom.toISOString(),
|
|
26556
|
+
validUntil: info.expiresAt.toISOString()
|
|
26557
|
+
};
|
|
26558
|
+
const envelope = yield* sealForUpload({
|
|
26559
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26560
|
+
credentialType: "pass-type-certificate",
|
|
26561
|
+
metadata,
|
|
26562
|
+
secret: {
|
|
26563
|
+
p12Base64: toBase64(bytes),
|
|
26564
|
+
p12Password: input.password
|
|
26565
|
+
}
|
|
26566
|
+
});
|
|
26567
|
+
return {
|
|
26568
|
+
id: (yield* api.applePassTypeCertificates.upload({ payload: {
|
|
26569
|
+
...toUploadEnvelope(envelope),
|
|
26570
|
+
...metadata
|
|
26571
|
+
} })).id,
|
|
26572
|
+
name: input.name,
|
|
26573
|
+
platform: "ios",
|
|
26574
|
+
type: "pass-type-certificate"
|
|
26575
|
+
};
|
|
26576
|
+
});
|
|
26577
|
+
|
|
26578
|
+
//#endregion
|
|
26579
|
+
//#region src/lib/credentials-pay-certificate.ts
|
|
26580
|
+
/**
|
|
26581
|
+
* Manual upload of an Apple Pay payment-processing `.p12` certificate. The
|
|
26582
|
+
* Merchant ID (`merchant.*`) is not carried reliably in the cert, so it is passed
|
|
26583
|
+
* explicitly; serial/validity come from the parsed cert and the team from its OU
|
|
26584
|
+
* (or `--apple-team-identifier`). Only `{ p12Base64, p12Password }` is sealed.
|
|
26585
|
+
*/
|
|
26586
|
+
const uploadIosPayCertificate = (api, input, bytes) => Effect.gen(function* () {
|
|
26587
|
+
if (input.password === void 0) return yield* new CredentialValidationError({ message: "Missing --password required for the selected credential type." });
|
|
26588
|
+
if (!input.merchantIdentifier) return yield* new CredentialValidationError({ message: "Missing --merchant-identifier required for an Apple Pay certificate." });
|
|
26589
|
+
const info = yield* inspectP12({
|
|
26590
|
+
data: Buffer.from(bytes),
|
|
26591
|
+
password: input.password
|
|
26592
|
+
});
|
|
26593
|
+
const appleTeamIdentifier = info.teamId ?? input.appleTeamIdentifier;
|
|
26594
|
+
if (!appleTeamIdentifier) return yield* new CredentialValidationError({ message: "Could not derive Apple Team ID from the certificate; pass --apple-team-identifier." });
|
|
26595
|
+
if (!info.validFrom || !info.expiresAt) return yield* new CredentialValidationError({ message: "Certificate is missing notBefore/notAfter dates." });
|
|
26596
|
+
const metadata = {
|
|
26597
|
+
merchantIdentifier: input.merchantIdentifier,
|
|
26598
|
+
serialNumber: info.serialNumber,
|
|
26599
|
+
appleTeamIdentifier,
|
|
26600
|
+
validFrom: info.validFrom.toISOString(),
|
|
26601
|
+
validUntil: info.expiresAt.toISOString()
|
|
26602
|
+
};
|
|
26603
|
+
const envelope = yield* sealForUpload({
|
|
26604
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26605
|
+
credentialType: "apple-pay-certificate",
|
|
26606
|
+
metadata,
|
|
26607
|
+
secret: {
|
|
26608
|
+
p12Base64: toBase64(bytes),
|
|
26609
|
+
p12Password: input.password
|
|
26610
|
+
}
|
|
26611
|
+
});
|
|
26612
|
+
return {
|
|
26613
|
+
id: (yield* api.applePayCertificates.upload({ payload: {
|
|
26614
|
+
...toUploadEnvelope(envelope),
|
|
26615
|
+
...metadata
|
|
26616
|
+
} })).id,
|
|
26617
|
+
name: input.name,
|
|
26618
|
+
platform: "ios",
|
|
26619
|
+
type: "apple-pay-certificate"
|
|
26620
|
+
};
|
|
26621
|
+
});
|
|
26622
|
+
|
|
26623
|
+
//#endregion
|
|
26624
|
+
//#region src/lib/credentials-push-certificate.ts
|
|
26625
|
+
/**
|
|
26626
|
+
* Derive the App ID a push SSL cert is bound to from its Common Name, e.g.
|
|
26627
|
+
* "Apple Push Services: com.example.app" → "com.example.app". Returns undefined
|
|
26628
|
+
* when the CN does not carry a reverse-DNS identifier.
|
|
26629
|
+
*/
|
|
26630
|
+
const bundleIdFromPushCertCN = (commonName) => {
|
|
26631
|
+
const bundle = /:\s*(?<bundle>[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?)\s*$/u.exec(commonName)?.groups?.["bundle"];
|
|
26632
|
+
return bundle?.includes(".") ? bundle : void 0;
|
|
26633
|
+
};
|
|
26634
|
+
/**
|
|
26635
|
+
* Manual upload of a legacy APNs Push Services `.p12` SSL certificate. The CLI
|
|
26636
|
+
* parses the cert locally for its metadata (serial, validity, team, and the App
|
|
26637
|
+
* ID from the CN) and seals `{ p12Base64, p12Password }` into the vault; the
|
|
26638
|
+
* server only ever stores the ciphertext.
|
|
26639
|
+
*/
|
|
26640
|
+
const uploadIosPushCertificate = (api, input, bytes) => Effect.gen(function* () {
|
|
26641
|
+
if (input.password === void 0) return yield* new CredentialValidationError({ message: "Missing --password required for the selected credential type." });
|
|
26642
|
+
const info = yield* inspectP12({
|
|
26643
|
+
data: Buffer.from(bytes),
|
|
26644
|
+
password: input.password
|
|
26645
|
+
});
|
|
26646
|
+
if (!info.teamId) return yield* new CredentialValidationError({ message: "Could not derive Apple Team ID from certificate subject (expected OU=TEAMID or CN with (TEAMID))." });
|
|
26647
|
+
if (!info.validFrom || !info.expiresAt) return yield* new CredentialValidationError({ message: "Certificate is missing notBefore/notAfter dates." });
|
|
26648
|
+
const bundleIdentifier = input.bundleIdentifier ?? bundleIdFromPushCertCN(info.signingIdentity);
|
|
26649
|
+
if (!bundleIdentifier) return yield* new CredentialValidationError({ message: "Could not derive the App ID from the push certificate (expected CN 'Apple Push Services: <bundle id>'). Pass --bundle-identifier." });
|
|
26650
|
+
const metadata = {
|
|
26651
|
+
bundleIdentifier,
|
|
26652
|
+
serialNumber: info.serialNumber,
|
|
26653
|
+
appleTeamIdentifier: info.teamId,
|
|
26654
|
+
validFrom: info.validFrom.toISOString(),
|
|
26655
|
+
validUntil: info.expiresAt.toISOString()
|
|
26656
|
+
};
|
|
26657
|
+
const envelope = yield* sealForUpload({
|
|
26658
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26659
|
+
credentialType: "push-certificate",
|
|
26660
|
+
metadata,
|
|
26661
|
+
secret: {
|
|
26662
|
+
p12Base64: toBase64(bytes),
|
|
26663
|
+
p12Password: input.password
|
|
26664
|
+
}
|
|
26665
|
+
});
|
|
26666
|
+
return {
|
|
26667
|
+
id: (yield* api.applePushCertificates.upload({ payload: {
|
|
26668
|
+
...toUploadEnvelope(envelope),
|
|
26669
|
+
...metadata
|
|
26670
|
+
} })).id,
|
|
26671
|
+
name: input.name,
|
|
26672
|
+
platform: "ios",
|
|
26673
|
+
type: "push-certificate"
|
|
26674
|
+
};
|
|
26675
|
+
});
|
|
26676
|
+
|
|
25861
26677
|
//#endregion
|
|
25862
26678
|
//#region src/lib/credentials-manager.ts
|
|
25863
26679
|
const formatDistribution = (value) => value.toLowerCase().replaceAll("_", "-");
|
|
25864
26680
|
const listAllCredentials = (api) => Effect.gen(function* () {
|
|
25865
|
-
const [certs, pushKeys, ascKeys, profiles, keystores, googleKeys] = yield* Effect.all([
|
|
26681
|
+
const [certs, pushKeys, pushCerts, payCerts, passCerts, ascKeys, profiles, keystores, googleKeys] = yield* Effect.all([
|
|
25866
26682
|
api.appleDistributionCertificates.list(),
|
|
25867
26683
|
api.applePushKeys.list(),
|
|
26684
|
+
api.applePushCertificates.list(),
|
|
26685
|
+
api.applePayCertificates.list(),
|
|
26686
|
+
api.applePassTypeCertificates.list(),
|
|
25868
26687
|
api.ascApiKeys.list(),
|
|
25869
26688
|
api.appleProvisioningProfiles.list({ urlParams: {} }),
|
|
25870
26689
|
api.androidUploadKeystores.list(),
|
|
@@ -25885,6 +26704,27 @@ const listAllCredentials = (api) => Effect.gen(function* () {
|
|
|
25885
26704
|
type: "push-key",
|
|
25886
26705
|
distribution: null
|
|
25887
26706
|
})),
|
|
26707
|
+
...pushCerts.items.map((cert) => ({
|
|
26708
|
+
id: cert.id,
|
|
26709
|
+
name: cert.bundleIdentifier,
|
|
26710
|
+
platform: "ios",
|
|
26711
|
+
type: "push-certificate",
|
|
26712
|
+
distribution: null
|
|
26713
|
+
})),
|
|
26714
|
+
...payCerts.items.map((cert) => ({
|
|
26715
|
+
id: cert.id,
|
|
26716
|
+
name: cert.merchantIdentifier,
|
|
26717
|
+
platform: "ios",
|
|
26718
|
+
type: "apple-pay-certificate",
|
|
26719
|
+
distribution: null
|
|
26720
|
+
})),
|
|
26721
|
+
...passCerts.items.map((cert) => ({
|
|
26722
|
+
id: cert.id,
|
|
26723
|
+
name: cert.passTypeIdentifier,
|
|
26724
|
+
platform: "ios",
|
|
26725
|
+
type: "pass-type-certificate",
|
|
26726
|
+
distribution: null
|
|
26727
|
+
})),
|
|
25888
26728
|
...ascKeys.items.map((key) => ({
|
|
25889
26729
|
id: key.id,
|
|
25890
26730
|
name: key.name,
|
|
@@ -26082,6 +26922,9 @@ const uploadAndroidGoogleServiceAccountKey = (api, input, bytes) => Effect.gen(f
|
|
|
26082
26922
|
const uploadHandlers = {
|
|
26083
26923
|
"ios:distribution-certificate": uploadIosDistributionCertificate,
|
|
26084
26924
|
"ios:push-key": uploadIosPushKey,
|
|
26925
|
+
"ios:push-certificate": uploadIosPushCertificate,
|
|
26926
|
+
"ios:apple-pay-certificate": uploadIosPayCertificate,
|
|
26927
|
+
"ios:pass-type-certificate": uploadIosPassTypeCertificate,
|
|
26085
26928
|
"ios:asc-api-key": uploadIosAscApiKey,
|
|
26086
26929
|
"ios:provisioning-profile": uploadIosProvisioningProfile,
|
|
26087
26930
|
"android:keystore": uploadAndroidKeystore,
|
|
@@ -26107,6 +26950,15 @@ const deleteCredential = (api, input) => {
|
|
|
26107
26950
|
platform: "ios",
|
|
26108
26951
|
type: "push-key"
|
|
26109
26952
|
}, () => api.applePushKeys.delete({ path })), Match.when({
|
|
26953
|
+
platform: "ios",
|
|
26954
|
+
type: "push-certificate"
|
|
26955
|
+
}, () => api.applePushCertificates.delete({ path })), Match.when({
|
|
26956
|
+
platform: "ios",
|
|
26957
|
+
type: "apple-pay-certificate"
|
|
26958
|
+
}, () => api.applePayCertificates.delete({ path })), Match.when({
|
|
26959
|
+
platform: "ios",
|
|
26960
|
+
type: "pass-type-certificate"
|
|
26961
|
+
}, () => api.applePassTypeCertificates.delete({ path })), Match.when({
|
|
26110
26962
|
platform: "ios",
|
|
26111
26963
|
type: "asc-api-key"
|
|
26112
26964
|
}, () => api.ascApiKeys.delete({ path })), Match.when({
|
|
@@ -26187,6 +27039,9 @@ const TYPE_LABELS = {
|
|
|
26187
27039
|
"distribution-certificate": "iOS distribution certificate",
|
|
26188
27040
|
"provisioning-profile": "iOS provisioning profile",
|
|
26189
27041
|
"push-key": "APNs push key",
|
|
27042
|
+
"push-certificate": "APNs push SSL certificate",
|
|
27043
|
+
"apple-pay-certificate": "Apple Pay certificate",
|
|
27044
|
+
"pass-type-certificate": "Pass Type ID certificate",
|
|
26190
27045
|
"asc-api-key": "ASC API key",
|
|
26191
27046
|
keystore: "Android keystore",
|
|
26192
27047
|
"google-service-account-key": "Google service account key"
|
|
@@ -27674,6 +28529,9 @@ const CREDENTIAL_TYPES$3 = [
|
|
|
27674
28529
|
"distribution-certificate",
|
|
27675
28530
|
"provisioning-profile",
|
|
27676
28531
|
"push-key",
|
|
28532
|
+
"push-certificate",
|
|
28533
|
+
"apple-pay-certificate",
|
|
28534
|
+
"pass-type-certificate",
|
|
27677
28535
|
"asc-api-key",
|
|
27678
28536
|
"keystore",
|
|
27679
28537
|
"google-service-account-key"
|
|
@@ -27790,6 +28648,9 @@ const DOWNLOAD_TYPES = [
|
|
|
27790
28648
|
"distribution-certificate",
|
|
27791
28649
|
"provisioning-profile",
|
|
27792
28650
|
"push-key",
|
|
28651
|
+
"push-certificate",
|
|
28652
|
+
"apple-pay-certificate",
|
|
28653
|
+
"pass-type-certificate",
|
|
27793
28654
|
"asc-api-key",
|
|
27794
28655
|
"keystore",
|
|
27795
28656
|
"google-service-account-key"
|
|
@@ -27881,6 +28742,105 @@ const downloadPushKey = ({ api, id, cwd, output }) => Effect.gen(function* () {
|
|
|
27881
28742
|
}
|
|
27882
28743
|
};
|
|
27883
28744
|
});
|
|
28745
|
+
const downloadPushCertificate = ({ api, id, cwd, output }) => Effect.gen(function* () {
|
|
28746
|
+
const data = yield* api.applePushCertificates.download({ path: { id } });
|
|
28747
|
+
const secret = yield* openFromDownload({
|
|
28748
|
+
session: yield* openVaultSessionInteractive(api),
|
|
28749
|
+
credentialType: "push-certificate",
|
|
28750
|
+
downloaded: data
|
|
28751
|
+
});
|
|
28752
|
+
const p12Base64 = yield* secretString(secret, "p12Base64");
|
|
28753
|
+
const p12Password = yield* secretString(secret, "p12Password");
|
|
28754
|
+
const filePath = resolveOutputPath(cwd, output, `${data.id}.p12`);
|
|
28755
|
+
yield* writeBinary(filePath, fromBase64(p12Base64));
|
|
28756
|
+
return {
|
|
28757
|
+
path: filePath,
|
|
28758
|
+
pairs: [
|
|
28759
|
+
["Path", filePath],
|
|
28760
|
+
["Type", "Apple push SSL certificate (.p12)"],
|
|
28761
|
+
["Bundle", data.bundleIdentifier],
|
|
28762
|
+
["Serial", data.serialNumber],
|
|
28763
|
+
["Apple team", data.appleTeamIdentifier],
|
|
28764
|
+
["Valid from", data.validFrom],
|
|
28765
|
+
["Valid until", data.validUntil],
|
|
28766
|
+
["P12 password", p12Password]
|
|
28767
|
+
],
|
|
28768
|
+
metadata: {
|
|
28769
|
+
bundleIdentifier: data.bundleIdentifier,
|
|
28770
|
+
serialNumber: data.serialNumber,
|
|
28771
|
+
appleTeamIdentifier: data.appleTeamIdentifier,
|
|
28772
|
+
validFrom: data.validFrom,
|
|
28773
|
+
validUntil: data.validUntil,
|
|
28774
|
+
p12Password
|
|
28775
|
+
}
|
|
28776
|
+
};
|
|
28777
|
+
});
|
|
28778
|
+
const downloadPayCertificate = ({ api, id, cwd, output }) => Effect.gen(function* () {
|
|
28779
|
+
const data = yield* api.applePayCertificates.download({ path: { id } });
|
|
28780
|
+
const secret = yield* openFromDownload({
|
|
28781
|
+
session: yield* openVaultSessionInteractive(api),
|
|
28782
|
+
credentialType: "apple-pay-certificate",
|
|
28783
|
+
downloaded: data
|
|
28784
|
+
});
|
|
28785
|
+
const p12Base64 = yield* secretString(secret, "p12Base64");
|
|
28786
|
+
const p12Password = yield* secretString(secret, "p12Password");
|
|
28787
|
+
const filePath = resolveOutputPath(cwd, output, `${data.id}.p12`);
|
|
28788
|
+
yield* writeBinary(filePath, fromBase64(p12Base64));
|
|
28789
|
+
return {
|
|
28790
|
+
path: filePath,
|
|
28791
|
+
pairs: [
|
|
28792
|
+
["Path", filePath],
|
|
28793
|
+
["Type", "Apple Pay payment processing certificate (.p12)"],
|
|
28794
|
+
["Merchant", data.merchantIdentifier],
|
|
28795
|
+
["Serial", data.serialNumber],
|
|
28796
|
+
["Apple team", data.appleTeamIdentifier],
|
|
28797
|
+
["Valid from", data.validFrom],
|
|
28798
|
+
["Valid until", data.validUntil],
|
|
28799
|
+
["P12 password", p12Password]
|
|
28800
|
+
],
|
|
28801
|
+
metadata: {
|
|
28802
|
+
merchantIdentifier: data.merchantIdentifier,
|
|
28803
|
+
serialNumber: data.serialNumber,
|
|
28804
|
+
appleTeamIdentifier: data.appleTeamIdentifier,
|
|
28805
|
+
validFrom: data.validFrom,
|
|
28806
|
+
validUntil: data.validUntil,
|
|
28807
|
+
p12Password
|
|
28808
|
+
}
|
|
28809
|
+
};
|
|
28810
|
+
});
|
|
28811
|
+
const downloadPassTypeCertificate = ({ api, id, cwd, output }) => Effect.gen(function* () {
|
|
28812
|
+
const data = yield* api.applePassTypeCertificates.download({ path: { id } });
|
|
28813
|
+
const secret = yield* openFromDownload({
|
|
28814
|
+
session: yield* openVaultSessionInteractive(api),
|
|
28815
|
+
credentialType: "pass-type-certificate",
|
|
28816
|
+
downloaded: data
|
|
28817
|
+
});
|
|
28818
|
+
const p12Base64 = yield* secretString(secret, "p12Base64");
|
|
28819
|
+
const p12Password = yield* secretString(secret, "p12Password");
|
|
28820
|
+
const filePath = resolveOutputPath(cwd, output, `${data.id}.p12`);
|
|
28821
|
+
yield* writeBinary(filePath, fromBase64(p12Base64));
|
|
28822
|
+
return {
|
|
28823
|
+
path: filePath,
|
|
28824
|
+
pairs: [
|
|
28825
|
+
["Path", filePath],
|
|
28826
|
+
["Type", "Wallet Pass Type ID certificate (.p12)"],
|
|
28827
|
+
["Pass Type ID", data.passTypeIdentifier],
|
|
28828
|
+
["Serial", data.serialNumber],
|
|
28829
|
+
["Apple team", data.appleTeamIdentifier],
|
|
28830
|
+
["Valid from", data.validFrom],
|
|
28831
|
+
["Valid until", data.validUntil],
|
|
28832
|
+
["P12 password", p12Password]
|
|
28833
|
+
],
|
|
28834
|
+
metadata: {
|
|
28835
|
+
passTypeIdentifier: data.passTypeIdentifier,
|
|
28836
|
+
serialNumber: data.serialNumber,
|
|
28837
|
+
appleTeamIdentifier: data.appleTeamIdentifier,
|
|
28838
|
+
validFrom: data.validFrom,
|
|
28839
|
+
validUntil: data.validUntil,
|
|
28840
|
+
p12Password
|
|
28841
|
+
}
|
|
28842
|
+
};
|
|
28843
|
+
});
|
|
27884
28844
|
const downloadAscApiKey$1 = ({ api, id, cwd, output }) => Effect.gen(function* () {
|
|
27885
28845
|
const data = yield* api.ascApiKeys.getCredentials({ path: { id } });
|
|
27886
28846
|
const p8Pem = yield* secretString(yield* openFromDownload({
|
|
@@ -27965,6 +28925,9 @@ const dispatchDownload = (ctx, type) => {
|
|
|
27965
28925
|
case "distribution-certificate": return downloadDistributionCertificate(ctx);
|
|
27966
28926
|
case "provisioning-profile": return downloadProvisioningProfile$1(ctx);
|
|
27967
28927
|
case "push-key": return downloadPushKey(ctx);
|
|
28928
|
+
case "push-certificate": return downloadPushCertificate(ctx);
|
|
28929
|
+
case "apple-pay-certificate": return downloadPayCertificate(ctx);
|
|
28930
|
+
case "pass-type-certificate": return downloadPassTypeCertificate(ctx);
|
|
27968
28931
|
case "asc-api-key": return downloadAscApiKey$1(ctx);
|
|
27969
28932
|
case "keystore": return downloadKeystore(ctx);
|
|
27970
28933
|
case "google-service-account-key": return downloadGoogleServiceAccountKey(ctx);
|
|
@@ -28010,6 +28973,99 @@ const downloadCommand = defineCommand({
|
|
|
28010
28973
|
}), { json: "value" })
|
|
28011
28974
|
});
|
|
28012
28975
|
|
|
28976
|
+
//#endregion
|
|
28977
|
+
//#region src/lib/credentials-generator-merchant.ts
|
|
28978
|
+
/**
|
|
28979
|
+
* Enable the Apple Pay capability on an App ID, registering the App ID first if
|
|
28980
|
+
* it does not exist yet. Returns once the capability is on.
|
|
28981
|
+
*/
|
|
28982
|
+
const enableApplePayCapability = (ctx, bundleIdentifier) => Effect.gen(function* () {
|
|
28983
|
+
const bundle = (yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }))) ?? (yield* wrap("apple-create-bundle-id", async () => AppleUtils.BundleId.createAsync(ctx, {
|
|
28984
|
+
identifier: bundleIdentifier,
|
|
28985
|
+
name: bundleIdentifier,
|
|
28986
|
+
platform: AppleUtils.BundleIdPlatform.IOS
|
|
28987
|
+
})));
|
|
28988
|
+
yield* wrap("apple-enable-apple-pay", async () => bundle.updateBundleIdCapabilityAsync({
|
|
28989
|
+
capabilityType: AppleUtils.CapabilityType.APPLE_PAY,
|
|
28990
|
+
option: AppleUtils.CapabilityTypeOption.ON
|
|
28991
|
+
}));
|
|
28992
|
+
});
|
|
28993
|
+
/**
|
|
28994
|
+
* Register an Apple Pay Merchant ID (`merchant.*`) on the Developer Portal via an
|
|
28995
|
+
* Apple ID session, optionally enabling the Apple Pay capability on an App ID.
|
|
28996
|
+
* This is the one piece of Apple Pay onboarding the portal API supports — the
|
|
28997
|
+
* payment-processing certificate itself is still created out-of-band (usually by
|
|
28998
|
+
* the PSP) and uploaded via `credentials upload --type apple-pay-certificate`.
|
|
28999
|
+
*/
|
|
29000
|
+
const registerMerchantIdViaAppleId = (input) => Effect.gen(function* () {
|
|
29001
|
+
const merchant = yield* wrap("apple-create-merchant-id", async () => AppleUtils.MerchantId.createAsync(input.context, {
|
|
29002
|
+
identifier: input.identifier,
|
|
29003
|
+
name: input.name
|
|
29004
|
+
}));
|
|
29005
|
+
if (input.bundleIdentifier !== void 0 && input.bundleIdentifier.length > 0) yield* enableApplePayCapability(input.context, input.bundleIdentifier);
|
|
29006
|
+
return {
|
|
29007
|
+
developerPortalIdentifier: merchant.id,
|
|
29008
|
+
identifier: input.identifier,
|
|
29009
|
+
name: input.name,
|
|
29010
|
+
capabilityEnabledForBundleId: input.bundleIdentifier
|
|
29011
|
+
};
|
|
29012
|
+
});
|
|
29013
|
+
|
|
29014
|
+
//#endregion
|
|
29015
|
+
//#region src/commands/credentials/generate-merchant-id.ts
|
|
29016
|
+
const MERCHANT_ID_PATTERN = /^merchant\.[A-Za-z0-9][A-Za-z0-9.-]*$/u;
|
|
29017
|
+
const MERCHANT_EXIT_EXTRAS = {
|
|
29018
|
+
CredentialValidationError: 2,
|
|
29019
|
+
AppleIdGenerateFailedError: 6,
|
|
29020
|
+
AppleAuthError: 4,
|
|
29021
|
+
InteractiveProhibitedError: 4
|
|
29022
|
+
};
|
|
29023
|
+
const merchantIdCommand = defineCommand({
|
|
29024
|
+
meta: {
|
|
29025
|
+
name: "merchant-id",
|
|
29026
|
+
description: "Register an Apple Pay Merchant ID (merchant.*) on the Developer Portal via Apple ID login, optionally turning on Apple Pay for an App ID. The payment-processing certificate itself is uploaded separately with `credentials upload --type apple-pay-certificate`."
|
|
29027
|
+
},
|
|
29028
|
+
args: {
|
|
29029
|
+
identifier: {
|
|
29030
|
+
type: "string",
|
|
29031
|
+
required: true,
|
|
29032
|
+
description: "Merchant ID (merchant.*)"
|
|
29033
|
+
},
|
|
29034
|
+
name: {
|
|
29035
|
+
type: "string",
|
|
29036
|
+
description: "Display name (defaults to the identifier)"
|
|
29037
|
+
},
|
|
29038
|
+
"bundle-identifier": {
|
|
29039
|
+
type: "string",
|
|
29040
|
+
description: "App ID to enable the Apple Pay capability on"
|
|
29041
|
+
}
|
|
29042
|
+
},
|
|
29043
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
29044
|
+
const identifier = args.identifier.trim();
|
|
29045
|
+
if (!MERCHANT_ID_PATTERN.test(identifier)) return yield* new CredentialValidationError({ message: `Merchant ID "${identifier}" must look like merchant.com.example.` });
|
|
29046
|
+
const auth = yield* AppleAuth;
|
|
29047
|
+
const session = yield* auth.ensureLoggedIn();
|
|
29048
|
+
yield* printHuman("Registering Apple Pay Merchant ID via your Apple ID...");
|
|
29049
|
+
const created = yield* registerMerchantIdViaAppleId({
|
|
29050
|
+
context: auth.buildRequestContext(session),
|
|
29051
|
+
identifier,
|
|
29052
|
+
name: args.name ?? identifier,
|
|
29053
|
+
...compact({ bundleIdentifier: args["bundle-identifier"] })
|
|
29054
|
+
});
|
|
29055
|
+
yield* printHuman("Merchant ID registered.");
|
|
29056
|
+
yield* printHumanKeyValue([
|
|
29057
|
+
["Merchant ID", created.identifier],
|
|
29058
|
+
["Name", created.name],
|
|
29059
|
+
["Apple identifier", created.developerPortalIdentifier],
|
|
29060
|
+
["Apple Pay enabled on", created.capabilityEnabledForBundleId ?? "-"]
|
|
29061
|
+
]);
|
|
29062
|
+
return created;
|
|
29063
|
+
}), {
|
|
29064
|
+
exits: MERCHANT_EXIT_EXTRAS,
|
|
29065
|
+
json: "value"
|
|
29066
|
+
})
|
|
29067
|
+
});
|
|
29068
|
+
|
|
28013
29069
|
//#endregion
|
|
28014
29070
|
//#region src/commands/credentials/generate-push-key.ts
|
|
28015
29071
|
const PUSH_KEY_EXIT_EXTRAS = {
|
|
@@ -28408,6 +29464,7 @@ const generateCommand$1 = defineCommand({
|
|
|
28408
29464
|
"distribution-certificate": distributionCertificateCommand$1,
|
|
28409
29465
|
"provisioning-profile": provisioningProfileCommand,
|
|
28410
29466
|
"push-key": pushKeyCommand$1,
|
|
29467
|
+
"merchant-id": merchantIdCommand,
|
|
28411
29468
|
"gsa-key": gsaKeyCommand
|
|
28412
29469
|
}
|
|
28413
29470
|
});
|
|
@@ -28708,6 +29765,9 @@ const CREDENTIAL_TYPES$2 = [
|
|
|
28708
29765
|
"distribution-certificate",
|
|
28709
29766
|
"provisioning-profile",
|
|
28710
29767
|
"push-key",
|
|
29768
|
+
"push-certificate",
|
|
29769
|
+
"apple-pay-certificate",
|
|
29770
|
+
"pass-type-certificate",
|
|
28711
29771
|
"asc-api-key",
|
|
28712
29772
|
"keystore",
|
|
28713
29773
|
"google-service-account-key"
|
|
@@ -29572,6 +30632,9 @@ const CREDENTIAL_TYPES$1 = [
|
|
|
29572
30632
|
"distribution-certificate",
|
|
29573
30633
|
"provisioning-profile",
|
|
29574
30634
|
"push-key",
|
|
30635
|
+
"push-certificate",
|
|
30636
|
+
"apple-pay-certificate",
|
|
30637
|
+
"pass-type-certificate",
|
|
29575
30638
|
"asc-api-key",
|
|
29576
30639
|
"keystore",
|
|
29577
30640
|
"google-service-account-key"
|
|
@@ -29625,6 +30688,18 @@ const uploadCommand = defineCommand({
|
|
|
29625
30688
|
"apple-team-identifier": {
|
|
29626
30689
|
type: "string",
|
|
29627
30690
|
description: "Apple Team ID"
|
|
30691
|
+
},
|
|
30692
|
+
"bundle-identifier": {
|
|
30693
|
+
type: "string",
|
|
30694
|
+
description: "App ID for a push certificate (else derived from the cert CN)"
|
|
30695
|
+
},
|
|
30696
|
+
"merchant-identifier": {
|
|
30697
|
+
type: "string",
|
|
30698
|
+
description: "Merchant ID (merchant.*) for an Apple Pay certificate"
|
|
30699
|
+
},
|
|
30700
|
+
"pass-type-identifier": {
|
|
30701
|
+
type: "string",
|
|
30702
|
+
description: "Pass Type ID (pass.*) for a Pass Type ID certificate"
|
|
29628
30703
|
}
|
|
29629
30704
|
},
|
|
29630
30705
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
@@ -29639,7 +30714,10 @@ const uploadCommand = defineCommand({
|
|
|
29639
30714
|
keyPassword: args["key-password"],
|
|
29640
30715
|
keyId: args["key-id"],
|
|
29641
30716
|
issuerId: args["issuer-id"],
|
|
29642
|
-
appleTeamIdentifier: args["apple-team-identifier"]
|
|
30717
|
+
appleTeamIdentifier: args["apple-team-identifier"],
|
|
30718
|
+
bundleIdentifier: args["bundle-identifier"],
|
|
30719
|
+
merchantIdentifier: args["merchant-identifier"],
|
|
30720
|
+
passTypeIdentifier: args["pass-type-identifier"]
|
|
29643
30721
|
})
|
|
29644
30722
|
});
|
|
29645
30723
|
yield* printHuman("Credential uploaded successfully.");
|
|
@@ -29713,6 +30791,9 @@ const CREDENTIAL_TYPES = [
|
|
|
29713
30791
|
"distribution-certificate",
|
|
29714
30792
|
"provisioning-profile",
|
|
29715
30793
|
"push-key",
|
|
30794
|
+
"push-certificate",
|
|
30795
|
+
"apple-pay-certificate",
|
|
30796
|
+
"pass-type-certificate",
|
|
29716
30797
|
"asc-api-key",
|
|
29717
30798
|
"keystore",
|
|
29718
30799
|
"google-service-account-key"
|
|
@@ -29777,6 +30858,66 @@ const viewPushKey = (api, id) => Effect.gen(function* () {
|
|
|
29777
30858
|
raw: item
|
|
29778
30859
|
};
|
|
29779
30860
|
});
|
|
30861
|
+
const viewPushCertificate = (api, id) => Effect.gen(function* () {
|
|
30862
|
+
const { items } = yield* api.applePushCertificates.list();
|
|
30863
|
+
const item = items.find((entry) => entry.id === id);
|
|
30864
|
+
if (!item) return yield* notFound(id, "push-certificate");
|
|
30865
|
+
return {
|
|
30866
|
+
kind: "push-certificate",
|
|
30867
|
+
pairs: [
|
|
30868
|
+
["ID", item.id],
|
|
30869
|
+
["Type", "Apple push SSL certificate"],
|
|
30870
|
+
["Bundle identifier", item.bundleIdentifier],
|
|
30871
|
+
["Serial number", item.serialNumber],
|
|
30872
|
+
["Apple team ID", item.appleTeamId],
|
|
30873
|
+
["Valid from", item.validFrom],
|
|
30874
|
+
["Valid until", item.validUntil],
|
|
30875
|
+
["Created", item.createdAt],
|
|
30876
|
+
["Updated", item.updatedAt]
|
|
30877
|
+
],
|
|
30878
|
+
raw: item
|
|
30879
|
+
};
|
|
30880
|
+
});
|
|
30881
|
+
const viewPayCertificate = (api, id) => Effect.gen(function* () {
|
|
30882
|
+
const { items } = yield* api.applePayCertificates.list();
|
|
30883
|
+
const item = items.find((entry) => entry.id === id);
|
|
30884
|
+
if (!item) return yield* notFound(id, "apple-pay-certificate");
|
|
30885
|
+
return {
|
|
30886
|
+
kind: "apple-pay-certificate",
|
|
30887
|
+
pairs: [
|
|
30888
|
+
["ID", item.id],
|
|
30889
|
+
["Type", "Apple Pay payment processing certificate"],
|
|
30890
|
+
["Merchant identifier", item.merchantIdentifier],
|
|
30891
|
+
["Serial number", item.serialNumber],
|
|
30892
|
+
["Apple team ID", item.appleTeamId],
|
|
30893
|
+
["Valid from", item.validFrom],
|
|
30894
|
+
["Valid until", item.validUntil],
|
|
30895
|
+
["Created", item.createdAt],
|
|
30896
|
+
["Updated", item.updatedAt]
|
|
30897
|
+
],
|
|
30898
|
+
raw: item
|
|
30899
|
+
};
|
|
30900
|
+
});
|
|
30901
|
+
const viewPassTypeCertificate = (api, id) => Effect.gen(function* () {
|
|
30902
|
+
const { items } = yield* api.applePassTypeCertificates.list();
|
|
30903
|
+
const item = items.find((entry) => entry.id === id);
|
|
30904
|
+
if (!item) return yield* notFound(id, "pass-type-certificate");
|
|
30905
|
+
return {
|
|
30906
|
+
kind: "pass-type-certificate",
|
|
30907
|
+
pairs: [
|
|
30908
|
+
["ID", item.id],
|
|
30909
|
+
["Type", "Wallet Pass Type ID certificate"],
|
|
30910
|
+
["Pass Type ID", item.passTypeIdentifier],
|
|
30911
|
+
["Serial number", item.serialNumber],
|
|
30912
|
+
["Apple team ID", item.appleTeamId],
|
|
30913
|
+
["Valid from", item.validFrom],
|
|
30914
|
+
["Valid until", item.validUntil],
|
|
30915
|
+
["Created", item.createdAt],
|
|
30916
|
+
["Updated", item.updatedAt]
|
|
30917
|
+
],
|
|
30918
|
+
raw: item
|
|
30919
|
+
};
|
|
30920
|
+
});
|
|
29780
30921
|
const viewAscApiKey = (api, id) => Effect.gen(function* () {
|
|
29781
30922
|
const { items } = yield* api.ascApiKeys.list();
|
|
29782
30923
|
const item = items.find((entry) => entry.id === id);
|
|
@@ -29835,6 +30976,9 @@ const lookupByType = (api, id, type) => {
|
|
|
29835
30976
|
case "distribution-certificate": return viewDistributionCertificate(api, id);
|
|
29836
30977
|
case "provisioning-profile": return viewProvisioningProfile(api, id);
|
|
29837
30978
|
case "push-key": return viewPushKey(api, id);
|
|
30979
|
+
case "push-certificate": return viewPushCertificate(api, id);
|
|
30980
|
+
case "apple-pay-certificate": return viewPayCertificate(api, id);
|
|
30981
|
+
case "pass-type-certificate": return viewPassTypeCertificate(api, id);
|
|
29838
30982
|
case "asc-api-key": return viewAscApiKey(api, id);
|
|
29839
30983
|
case "keystore": return viewKeystore(api, id);
|
|
29840
30984
|
case "google-service-account-key": return viewGoogleServiceAccountKey(api, id);
|
|
@@ -32023,6 +33167,20 @@ const persistLink = (projectRoot, projectId, hasExpoConfig) => Effect.gen(functi
|
|
|
32023
33167
|
configPath: filePath
|
|
32024
33168
|
};
|
|
32025
33169
|
});
|
|
33170
|
+
/**
|
|
33171
|
+
* Scaffold the default `eas.json` build profiles after linking so a freshly
|
|
33172
|
+
* `init`ed project can `build` straight away. Only acts when no `build` section
|
|
33173
|
+
* exists yet — a project that already defines profiles is left untouched (run
|
|
33174
|
+
* `build configure` to top those up). Use `build configure` to re-scaffold.
|
|
33175
|
+
*/
|
|
33176
|
+
const scaffoldBuildProfiles = (projectRoot) => Effect.gen(function* () {
|
|
33177
|
+
const existing = yield* readEasJsonRaw(projectRoot);
|
|
33178
|
+
const existingBuild = isRecord$1(existing?.["build"]) ? existing["build"] : {};
|
|
33179
|
+
if (Object.keys(existingBuild).length > 0) return [];
|
|
33180
|
+
const result = yield* ensureDefaultBuildProfiles(projectRoot);
|
|
33181
|
+
yield* printHuman(`Scaffolded eas.json with default build profiles: ${result.added.join(", ")}.`);
|
|
33182
|
+
return result.added;
|
|
33183
|
+
});
|
|
32026
33184
|
const initCommand = defineCommand({
|
|
32027
33185
|
meta: {
|
|
32028
33186
|
name: "init",
|
|
@@ -32050,9 +33208,12 @@ const initCommand = defineCommand({
|
|
|
32050
33208
|
if (args.id !== void 0 && args.id.length > 0) {
|
|
32051
33209
|
const project = yield* api.projects.get({ path: { id: args.id } });
|
|
32052
33210
|
yield* printHuman(`Linking project: ${project.name} (${project.id})`);
|
|
33211
|
+
const linked = yield* persistLink(projectRoot, project.id, hasExpoConfig);
|
|
33212
|
+
const buildProfiles = yield* scaffoldBuildProfiles(projectRoot);
|
|
32053
33213
|
return {
|
|
32054
33214
|
linked: true,
|
|
32055
|
-
...
|
|
33215
|
+
...linked,
|
|
33216
|
+
buildProfiles
|
|
32056
33217
|
};
|
|
32057
33218
|
}
|
|
32058
33219
|
const { name, slug } = yield* resolveNameAndSlug(args, projectRoot, expoConfig);
|
|
@@ -32066,21 +33227,24 @@ const initCommand = defineCommand({
|
|
|
32066
33227
|
limit: 100
|
|
32067
33228
|
} });
|
|
32068
33229
|
const existing = items.find((project) => project.slug === slug);
|
|
33230
|
+
const linked = yield* persistLink(projectRoot, yield* Effect.gen(function* () {
|
|
33231
|
+
if (existing) {
|
|
33232
|
+
yield* printHuman(`Found existing project: ${existing.name} (${existing.id})`);
|
|
33233
|
+
return existing.id;
|
|
33234
|
+
}
|
|
33235
|
+
yield* printHuman("No existing project found. Creating new project...");
|
|
33236
|
+
const created = yield* api.projects.create({ payload: {
|
|
33237
|
+
name,
|
|
33238
|
+
slug
|
|
33239
|
+
} });
|
|
33240
|
+
yield* printHuman(`Created project: ${created.name} (${created.id})`);
|
|
33241
|
+
return created.id;
|
|
33242
|
+
}), hasExpoConfig);
|
|
33243
|
+
const buildProfiles = yield* scaffoldBuildProfiles(projectRoot);
|
|
32069
33244
|
return {
|
|
32070
33245
|
linked: true,
|
|
32071
|
-
...
|
|
32072
|
-
|
|
32073
|
-
yield* printHuman(`Found existing project: ${existing.name} (${existing.id})`);
|
|
32074
|
-
return existing.id;
|
|
32075
|
-
}
|
|
32076
|
-
yield* printHuman("No existing project found. Creating new project...");
|
|
32077
|
-
const created = yield* api.projects.create({ payload: {
|
|
32078
|
-
name,
|
|
32079
|
-
slug
|
|
32080
|
-
} });
|
|
32081
|
-
yield* printHuman(`Created project: ${created.name} (${created.id})`);
|
|
32082
|
-
return created.id;
|
|
32083
|
-
}), hasExpoConfig)
|
|
33246
|
+
...linked,
|
|
33247
|
+
buildProfiles
|
|
32084
33248
|
};
|
|
32085
33249
|
}), { json: "value" })
|
|
32086
33250
|
});
|
|
@@ -32709,17 +33873,33 @@ const runFlow = (api, projectId, args) => Effect.gen(function* () {
|
|
|
32709
33873
|
});
|
|
32710
33874
|
yield* printHuman(`Submission created: ${submission.id} (${submission.status})`);
|
|
32711
33875
|
if (args.platform === "ios" && iosConfig !== void 0) {
|
|
32712
|
-
const
|
|
32713
|
-
|
|
32714
|
-
|
|
33876
|
+
const iosProfile = args.easProfile.ios;
|
|
33877
|
+
const auth = resolveIosUploadAuth({
|
|
33878
|
+
appleId: iosProfile?.appleId,
|
|
33879
|
+
ascApiKeyId: iosProfile?.ascApiKeyId,
|
|
33880
|
+
hasAppSpecificPassword: hasAppleAppSpecificPassword()
|
|
33881
|
+
});
|
|
33882
|
+
if (auth === null) {
|
|
33883
|
+
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.");
|
|
32715
33884
|
return submission;
|
|
32716
33885
|
}
|
|
32717
|
-
yield* printHuman("Running xcrun altool upload
|
|
32718
|
-
yield*
|
|
33886
|
+
yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
|
|
33887
|
+
yield* runIosSubmit({
|
|
32719
33888
|
api,
|
|
32720
33889
|
submissionId: submission.id,
|
|
32721
|
-
|
|
32722
|
-
|
|
33890
|
+
archive: {
|
|
33891
|
+
source: args.archive.archiveSource,
|
|
33892
|
+
value: args.archive.archiveUrl
|
|
33893
|
+
},
|
|
33894
|
+
auth,
|
|
33895
|
+
ascApiKeyId: iosProfile?.ascApiKeyId,
|
|
33896
|
+
config: {
|
|
33897
|
+
bundleIdentifier: iosConfig.bundleIdentifier,
|
|
33898
|
+
ascAppId: iosProfile?.ascAppId,
|
|
33899
|
+
language: iosProfile?.language,
|
|
33900
|
+
whatToTest: args.whatToTest,
|
|
33901
|
+
groups: iosProfile?.groups ?? []
|
|
33902
|
+
}
|
|
32723
33903
|
});
|
|
32724
33904
|
}
|
|
32725
33905
|
if (args.platform === "android" && args.easProfile.android !== void 0) {
|
|
@@ -35897,10 +37077,11 @@ const bootstrapVersionCheck = (currentVersion, installerHint, spawnRefresh, opti
|
|
|
35897
37077
|
if (yield* isOptedOut) return;
|
|
35898
37078
|
const versionCheck = yield* VersionCheck;
|
|
35899
37079
|
if (options?.quiet !== true) {
|
|
35900
|
-
|
|
35901
|
-
if (
|
|
37080
|
+
let latest = yield* versionCheck.cachedLatest;
|
|
37081
|
+
if (latest === void 0) latest = yield* versionCheck.fetchLatest;
|
|
37082
|
+
if (latest && isNewerVersion(latest, currentVersion)) {
|
|
35902
37083
|
const installer = detectInstallerFromImportMetaUrl(installerHint);
|
|
35903
|
-
yield* Console.error(formatNotice(currentVersion,
|
|
37084
|
+
yield* Console.error(formatNotice(currentVersion, latest, installCommand(installer)));
|
|
35904
37085
|
}
|
|
35905
37086
|
}
|
|
35906
37087
|
if (yield* versionCheck.cacheStale) spawnRefresh();
|