@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 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.37.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. Five signing-credential tables plus `envVarValue` (one
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 = 1440 * 60 * 1e3;
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
- refreshCache: Effect.gen(function* () {
3599
- const request = HttpClientRequest.get(NPM_REGISTRY_URL).pipe(HttpClientRequest.setHeader("accept", "application/json"));
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
- status: params.releaseStatus,
20141
- versionCodes: [String(params.versionCode)],
20142
- ...compact({
20143
- userFraction: toOptional(params.rollout),
20144
- releaseNotes: params.releaseNotes ? [{
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 writeAscApiKeyP8 = (api, ascApiKeyId) => Effect.gen(function* () {
20235
- const creds = yield* fetchAscCredentials(api, ascApiKeyId).pipe(Effect.mapError(() => new CliSubmitError({
20236
- code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
20237
- message: `Failed to fetch or decrypt ASC API key ${ascApiKeyId}`
20238
- })));
20239
- const target = path.join(tmpdir(), `better-update-submit-AuthKey_${creds.keyId}.p8`);
20240
- yield* Effect.promise(async () => writeFile(target, creds.p8Pem, "utf8"));
20241
- return {
20242
- p8Path: target,
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 AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
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 AAB at ${archive.value}: ${cause instanceof Error ? cause.message : String(cause)}`), (buf) => new Uint8Array(buf)) : fetchArchiveOverHttp(archive.value);
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 Effect.map(readLocalFile(params.serviceAccountKeyPath, "SUBMISSION_ANDROID_SA_KEY_LOCAL_READ_FAILED", (cause) => `Failed to read service account JSON at ${String(params.serviceAccountKeyPath)}: ${cause instanceof Error ? cause.message : String(cause)}`), (buf) => new TextDecoder().decode(buf));
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: inputs.androidProfile.releaseStatus ?? "completed",
21393
+ releaseStatus,
20413
21394
  changesNotSentForReview: inputs.androidProfile.changesNotSentForReview ?? false,
20414
- rollout: toDbNull(inputs.androidProfile.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" && easProfile.ios?.ascApiKeyId !== void 0) {
20477
- yield* printHuman("Running xcrun altool upload...");
20478
- yield* runIosAltoolUpload({
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
- ipaPath: archiveUrl,
20482
- ascApiKeyId: easProfile.ios.ascApiKeyId
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 easJsonPath = path.join(projectRoot, "eas.json");
24934
+ const filePath = easJsonPath(projectRoot);
24257
24935
  const fs = yield* FileSystem.FileSystem;
24258
- if (!(yield* fs.exists(easJsonPath))) {
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 ${easJsonPath} with defaults?`) : true)) {
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: easJsonPath
24942
+ path: filePath
24274
24943
  };
24275
24944
  }
24276
- yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
24277
- yield* printHuman(`Overwrote eas.json with default profiles.`);
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: easJsonPath,
24281
- profiles: [...DEFAULT_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(easJsonPath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to read eas.json: ${cause.message}` }))));
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 = DEFAULT_PROFILES.filter((name) => !existingProfiles.includes(name));
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: easJsonPath,
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: easJsonPath
24979
+ path: filePath
24301
24980
  };
24302
24981
  }
24303
- const additions = Object.fromEntries(missing.map((name) => [name, DEFAULT_EAS_JSON.build[name]]));
24304
- yield* writeEasJson(easJsonPath, {
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", missing.join(", ")],
24315
- ["Path", easJsonPath]
24986
+ ["Added", result.added.join(", ")],
24987
+ ["Path", result.path]
24316
24988
  ]);
24317
24989
  return {
24318
24990
  action: "topped-up",
24319
- path: easJsonPath,
24991
+ path: result.path,
24320
24992
  existing: existingProfiles,
24321
- added: [...missing]
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
- ...yield* persistLink(projectRoot, project.id, hasExpoConfig)
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
- ...yield* persistLink(projectRoot, yield* Effect.gen(function* () {
32072
- if (existing) {
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 ascApiKeyId = args.easProfile.ios?.ascApiKeyId;
32713
- if (ascApiKeyId === void 0) {
32714
- yield* printHuman("iOS submission queued. Resolve ascApiKeyId in eas.json submit profile to enable client-side altool upload.");
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 locally...");
32718
- yield* runIosAltoolUpload({
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
- ipaPath: args.archive.archiveUrl,
32722
- ascApiKeyId
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
- const cached = yield* versionCheck.cachedLatest;
35901
- if (cached && isNewerVersion(cached, currentVersion)) {
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, cached, installCommand(installer)));
37084
+ yield* Console.error(formatNotice(currentVersion, latest, installCommand(installer)));
35904
37085
  }
35905
37086
  }
35906
37087
  if (yield* versionCheck.cacheStale) spawnRefresh();