@better-update/cli 0.19.0 → 0.20.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
@@ -17,11 +17,12 @@ import forge from "node-forge";
17
17
  import { accessSync, chmodSync, constants, createReadStream, existsSync, promises, readFileSync, writeFileSync } from "node:fs";
18
18
  import { spawn as spawn$1 } from "node-pty";
19
19
  import chalk from "chalk";
20
- import os from "node:os";
20
+ import os, { tmpdir } from "node:os";
21
21
  import plistMod from "@expo/plist";
22
22
  import { ExpoRunFormatter } from "@expo/xcpretty";
23
23
  import { promisify } from "node:util";
24
24
  import ignore from "ignore";
25
+ import { readFile, writeFile } from "node:fs/promises";
25
26
  import { Buffer as Buffer$1 } from "node:buffer";
26
27
  import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
27
28
  import qrcode from "qrcode-terminal";
@@ -32,7 +33,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
32
33
 
33
34
  //#endregion
34
35
  //#region package.json
35
- var version = "0.19.0";
36
+ var version = "0.20.0";
36
37
 
37
38
  //#endregion
38
39
  //#region src/lib/interactive-mode.ts
@@ -200,15 +201,15 @@ var NotAcceptable = class extends Schema.TaggedError()("NotAcceptable", { messag
200
201
 
201
202
  //#endregion
202
203
  //#region ../../packages/api/src/groups/android-application-identifiers.ts
203
- const idParam$16 = HttpApiSchema.param("id", Schema.String);
204
- const projectIdParam$4 = HttpApiSchema.param("projectId", Schema.String);
205
- var AndroidApplicationIdentifiersGroup = class extends HttpApiGroup.make("androidApplicationIdentifiers").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$4}/android-application-identifiers`.addSuccess(Schema.Struct({ items: Schema.Array(AndroidApplicationIdentifier) })).annotateContext(OpenApi.annotations({
204
+ const idParam$18 = HttpApiSchema.param("id", Schema.String);
205
+ const projectIdParam$6 = HttpApiSchema.param("projectId", Schema.String);
206
+ var AndroidApplicationIdentifiersGroup = class extends HttpApiGroup.make("androidApplicationIdentifiers").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$6}/android-application-identifiers`.addSuccess(Schema.Struct({ items: Schema.Array(AndroidApplicationIdentifier) })).annotateContext(OpenApi.annotations({
206
207
  title: "List Android application identifiers",
207
208
  description: "List all Android package identifiers for a project"
208
- }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$4}/android-application-identifiers`.setPayload(CreateAndroidApplicationIdentifierBody).addSuccess(AndroidApplicationIdentifier, { status: 201 }).annotateContext(OpenApi.annotations({
209
+ }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$6}/android-application-identifiers`.setPayload(CreateAndroidApplicationIdentifierBody).addSuccess(AndroidApplicationIdentifier, { status: 201 }).annotateContext(OpenApi.annotations({
209
210
  title: "Create Android application identifier",
210
211
  description: "Register an Android package name for a project"
211
- }))).add(HttpApiEndpoint.del("delete")`/api/android-application-identifiers/${idParam$16}`.addSuccess(DeleteAndroidApplicationIdentifierResult).annotateContext(OpenApi.annotations({
212
+ }))).add(HttpApiEndpoint.del("delete")`/api/android-application-identifiers/${idParam$18}`.addSuccess(DeleteAndroidApplicationIdentifierResult).annotateContext(OpenApi.annotations({
212
213
  title: "Delete Android application identifier",
213
214
  description: "Remove an Android application identifier"
214
215
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -248,7 +249,7 @@ const DeleteAndroidBuildCredentialsResult = Schema.Struct({ deleted: Schema.Numb
248
249
 
249
250
  //#endregion
250
251
  //#region ../../packages/api/src/groups/android-build-credentials.ts
251
- const idParam$15 = HttpApiSchema.param("id", Schema.String);
252
+ const idParam$17 = HttpApiSchema.param("id", Schema.String);
252
253
  const applicationIdentifierIdParam = HttpApiSchema.param("applicationIdentifierId", Schema.String);
253
254
  var AndroidBuildCredentialsGroup = class extends HttpApiGroup.make("androidBuildCredentials").add(HttpApiEndpoint.get("list")`/api/android-application-identifiers/${applicationIdentifierIdParam}/build-credentials`.addSuccess(Schema.Struct({ items: Schema.Array(AndroidBuildCredentials) })).annotateContext(OpenApi.annotations({
254
255
  title: "List Android build credentials",
@@ -256,10 +257,10 @@ var AndroidBuildCredentialsGroup = class extends HttpApiGroup.make("androidBuild
256
257
  }))).add(HttpApiEndpoint.post("create")`/api/android-application-identifiers/${applicationIdentifierIdParam}/build-credentials`.setPayload(CreateAndroidBuildCredentialsBody).addSuccess(AndroidBuildCredentials, { status: 201 }).annotateContext(OpenApi.annotations({
257
258
  title: "Create Android build credentials group",
258
259
  description: "Create a named build credentials group (Default or custom)"
259
- }))).add(HttpApiEndpoint.put("update")`/api/android-build-credentials/${idParam$15}`.setPayload(UpdateAndroidBuildCredentialsBody).addSuccess(AndroidBuildCredentials).annotateContext(OpenApi.annotations({
260
+ }))).add(HttpApiEndpoint.put("update")`/api/android-build-credentials/${idParam$17}`.setPayload(UpdateAndroidBuildCredentialsBody).addSuccess(AndroidBuildCredentials).annotateContext(OpenApi.annotations({
260
261
  title: "Update Android build credentials",
261
262
  description: "Rename group, change default flag, or swap bound keystore/keys"
262
- }))).add(HttpApiEndpoint.del("delete")`/api/android-build-credentials/${idParam$15}`.addSuccess(DeleteAndroidBuildCredentialsResult).annotateContext(OpenApi.annotations({
263
+ }))).add(HttpApiEndpoint.del("delete")`/api/android-build-credentials/${idParam$17}`.addSuccess(DeleteAndroidBuildCredentialsResult).annotateContext(OpenApi.annotations({
263
264
  title: "Delete Android build credentials",
264
265
  description: "Remove a build credentials group"
265
266
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -299,17 +300,17 @@ const DownloadAndroidUploadKeystoreResult = Schema.Struct({
299
300
 
300
301
  //#endregion
301
302
  //#region ../../packages/api/src/groups/android-upload-keystores.ts
302
- const idParam$14 = HttpApiSchema.param("id", Schema.String);
303
+ const idParam$16 = HttpApiSchema.param("id", Schema.String);
303
304
  var AndroidUploadKeystoresGroup = class extends HttpApiGroup.make("androidUploadKeystores").add(HttpApiEndpoint.get("list", "/api/android/upload-keystores").addSuccess(Schema.Struct({ items: Schema.Array(AndroidUploadKeystore) })).annotateContext(OpenApi.annotations({
304
305
  title: "List Android upload keystores",
305
306
  description: "List uploaded Android keystores"
306
307
  }))).add(HttpApiEndpoint.post("upload", "/api/android/upload-keystores").setPayload(UploadAndroidUploadKeystoreBody).addSuccess(AndroidUploadKeystore, { status: 201 }).annotateContext(OpenApi.annotations({
307
308
  title: "Upload Android keystore",
308
309
  description: "Upload a JKS/PKCS12 keystore with key alias + passwords"
309
- }))).add(HttpApiEndpoint.del("delete")`/api/android/upload-keystores/${idParam$14}`.addSuccess(DeleteAndroidUploadKeystoreResult).annotateContext(OpenApi.annotations({
310
+ }))).add(HttpApiEndpoint.del("delete")`/api/android/upload-keystores/${idParam$16}`.addSuccess(DeleteAndroidUploadKeystoreResult).annotateContext(OpenApi.annotations({
310
311
  title: "Delete Android keystore",
311
312
  description: "Remove a stored Android keystore"
312
- }))).add(HttpApiEndpoint.get("download")`/api/android/upload-keystores/${idParam$14}/download`.addSuccess(DownloadAndroidUploadKeystoreResult).annotateContext(OpenApi.annotations({
313
+ }))).add(HttpApiEndpoint.get("download")`/api/android/upload-keystores/${idParam$16}/download`.addSuccess(DownloadAndroidUploadKeystoreResult).annotateContext(OpenApi.annotations({
313
314
  title: "Download Android keystore",
314
315
  description: "Fetch the decrypted keystore + passwords for local use (audit-logged)"
315
316
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -373,17 +374,17 @@ const DownloadAppleDistributionCertificateResult = Schema.Struct({
373
374
 
374
375
  //#endregion
375
376
  //#region ../../packages/api/src/groups/apple-distribution-certificates.ts
376
- const idParam$13 = HttpApiSchema.param("id", Schema.String);
377
+ const idParam$15 = HttpApiSchema.param("id", Schema.String);
377
378
  var AppleDistributionCertificatesGroup = class extends HttpApiGroup.make("appleDistributionCertificates").add(HttpApiEndpoint.get("list", "/api/apple/distribution-certificates").addSuccess(Schema.Struct({ items: Schema.Array(AppleDistributionCertificate) })).annotateContext(OpenApi.annotations({
378
379
  title: "List Apple distribution certificates",
379
380
  description: "List uploaded Apple distribution certificates for the organization"
380
381
  }))).add(HttpApiEndpoint.post("upload", "/api/apple/distribution-certificates").setPayload(UploadAppleDistributionCertificateBody).addSuccess(AppleDistributionCertificate, { status: 201 }).annotateContext(OpenApi.annotations({
381
382
  title: "Upload distribution certificate",
382
383
  description: "Upload a .p12 distribution certificate; auto-derives the Apple team from the provided identifier"
383
- }))).add(HttpApiEndpoint.del("delete")`/api/apple/distribution-certificates/${idParam$13}`.addSuccess(DeleteAppleDistributionCertificateResult).annotateContext(OpenApi.annotations({
384
+ }))).add(HttpApiEndpoint.del("delete")`/api/apple/distribution-certificates/${idParam$15}`.addSuccess(DeleteAppleDistributionCertificateResult).annotateContext(OpenApi.annotations({
384
385
  title: "Delete distribution certificate",
385
386
  description: "Remove a distribution certificate from storage"
386
- }))).add(HttpApiEndpoint.get("download")`/api/apple/distribution-certificates/${idParam$13}/download`.addSuccess(DownloadAppleDistributionCertificateResult).annotateContext(OpenApi.annotations({
387
+ }))).add(HttpApiEndpoint.get("download")`/api/apple/distribution-certificates/${idParam$15}/download`.addSuccess(DownloadAppleDistributionCertificateResult).annotateContext(OpenApi.annotations({
387
388
  title: "Download distribution certificate",
388
389
  description: "Fetch the decrypted .p12 + password for local use (audit-logged)"
389
390
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -431,17 +432,17 @@ const ListAppleProvisioningProfilesParams = Schema.Struct({
431
432
 
432
433
  //#endregion
433
434
  //#region ../../packages/api/src/groups/apple-provisioning-profiles.ts
434
- const idParam$12 = HttpApiSchema.param("id", Schema.String);
435
+ const idParam$14 = HttpApiSchema.param("id", Schema.String);
435
436
  var AppleProvisioningProfilesGroup = class extends HttpApiGroup.make("appleProvisioningProfiles").add(HttpApiEndpoint.get("list", "/api/apple/provisioning-profiles").setUrlParams(ListAppleProvisioningProfilesParams).addSuccess(Schema.Struct({ items: Schema.Array(AppleProvisioningProfile) })).annotateContext(OpenApi.annotations({
436
437
  title: "List provisioning profiles",
437
438
  description: "List stored provisioning profiles, optionally filtered by bundle + team"
438
439
  }))).add(HttpApiEndpoint.post("upload", "/api/apple/provisioning-profiles").setPayload(UploadAppleProvisioningProfileBody).addSuccess(AppleProvisioningProfile, { status: 201 }).annotateContext(OpenApi.annotations({
439
440
  title: "Upload provisioning profile",
440
441
  description: "Upload an existing or freshly generated .mobileprovision; auto-parses the embedded plist"
441
- }))).add(HttpApiEndpoint.del("delete")`/api/apple/provisioning-profiles/${idParam$12}`.addSuccess(DeleteAppleProvisioningProfileResult).annotateContext(OpenApi.annotations({
442
+ }))).add(HttpApiEndpoint.del("delete")`/api/apple/provisioning-profiles/${idParam$14}`.addSuccess(DeleteAppleProvisioningProfileResult).annotateContext(OpenApi.annotations({
442
443
  title: "Delete provisioning profile",
443
444
  description: "Remove a stored provisioning profile"
444
- }))).add(HttpApiEndpoint.get("download")`/api/apple/provisioning-profiles/${idParam$12}/download`.addSuccess(DownloadAppleProvisioningProfileResult).annotateContext(OpenApi.annotations({
445
+ }))).add(HttpApiEndpoint.get("download")`/api/apple/provisioning-profiles/${idParam$14}/download`.addSuccess(DownloadAppleProvisioningProfileResult).annotateContext(OpenApi.annotations({
445
446
  title: "Download provisioning profile",
446
447
  description: "Fetch the decoded .mobileprovision for local use (audit-logged)"
447
448
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -477,17 +478,17 @@ const DownloadApplePushKeyResult = Schema.Struct({
477
478
 
478
479
  //#endregion
479
480
  //#region ../../packages/api/src/groups/apple-push-keys.ts
480
- const idParam$11 = HttpApiSchema.param("id", Schema.String);
481
+ const idParam$13 = HttpApiSchema.param("id", Schema.String);
481
482
  var ApplePushKeysGroup = class extends HttpApiGroup.make("applePushKeys").add(HttpApiEndpoint.get("list", "/api/apple/push-keys").addSuccess(Schema.Struct({ items: Schema.Array(ApplePushKey) })).annotateContext(OpenApi.annotations({
482
483
  title: "List Apple push keys",
483
484
  description: "List APNs push keys for the organization"
484
485
  }))).add(HttpApiEndpoint.post("upload", "/api/apple/push-keys").setPayload(UploadApplePushKeyBody).addSuccess(ApplePushKey, { status: 201 }).annotateContext(OpenApi.annotations({
485
486
  title: "Upload push key",
486
487
  description: "Upload an APNs .p8 push notification key"
487
- }))).add(HttpApiEndpoint.del("delete")`/api/apple/push-keys/${idParam$11}`.addSuccess(DeleteApplePushKeyResult).annotateContext(OpenApi.annotations({
488
+ }))).add(HttpApiEndpoint.del("delete")`/api/apple/push-keys/${idParam$13}`.addSuccess(DeleteApplePushKeyResult).annotateContext(OpenApi.annotations({
488
489
  title: "Delete push key",
489
490
  description: "Remove a stored APNs push key"
490
- }))).add(HttpApiEndpoint.get("download")`/api/apple/push-keys/${idParam$11}/download`.addSuccess(DownloadApplePushKeyResult).annotateContext(OpenApi.annotations({
491
+ }))).add(HttpApiEndpoint.get("download")`/api/apple/push-keys/${idParam$13}/download`.addSuccess(DownloadApplePushKeyResult).annotateContext(OpenApi.annotations({
491
492
  title: "Download push key",
492
493
  description: "Fetch the decrypted .p8 push key for local use (audit-logged)"
493
494
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -514,6 +515,7 @@ var AscApiKey = class extends Schema.Class("AscApiKey")({
514
515
  organizationId: Id,
515
516
  appleTeamId: Schema.NullOr(Id),
516
517
  keyId: Schema.String,
518
+ issuerId: IssuerId,
517
519
  name: Schema.String,
518
520
  roles: Schema.Array(Schema.String),
519
521
  createdAt: DateTimeString,
@@ -560,20 +562,20 @@ const SyncDevicesResult = Schema.Struct({
560
562
 
561
563
  //#endregion
562
564
  //#region ../../packages/api/src/groups/asc-api-keys.ts
563
- const idParam$10 = HttpApiSchema.param("id", Schema.String);
565
+ const idParam$12 = HttpApiSchema.param("id", Schema.String);
564
566
  var AscApiKeysGroup = class extends HttpApiGroup.make("ascApiKeys").add(HttpApiEndpoint.get("list", "/api/apple/asc-api-keys").addSuccess(Schema.Struct({ items: Schema.Array(AscApiKey) })).annotateContext(OpenApi.annotations({
565
567
  title: "List App Store Connect API keys",
566
568
  description: "List stored ASC API keys for the organization"
567
569
  }))).add(HttpApiEndpoint.post("upload", "/api/apple/asc-api-keys").setPayload(UploadAscApiKeyBody).addSuccess(AscApiKey, { status: 201 }).annotateContext(OpenApi.annotations({
568
570
  title: "Upload ASC API key",
569
571
  description: "Upload an App Store Connect API key (.p8 + issuer)"
570
- }))).add(HttpApiEndpoint.del("delete")`/api/apple/asc-api-keys/${idParam$10}`.addSuccess(DeleteAscApiKeyResult).annotateContext(OpenApi.annotations({
572
+ }))).add(HttpApiEndpoint.del("delete")`/api/apple/asc-api-keys/${idParam$12}`.addSuccess(DeleteAscApiKeyResult).annotateContext(OpenApi.annotations({
571
573
  title: "Delete ASC API key",
572
574
  description: "Remove a stored ASC API key"
573
- }))).add(HttpApiEndpoint.post("syncDevices")`/api/apple/asc-api-keys/${idParam$10}/sync-devices`.addSuccess(SyncDevicesResult).annotateContext(OpenApi.annotations({
575
+ }))).add(HttpApiEndpoint.post("syncDevices")`/api/apple/asc-api-keys/${idParam$12}/sync-devices`.addSuccess(SyncDevicesResult).annotateContext(OpenApi.annotations({
574
576
  title: "Sync devices via ASC API key",
575
577
  description: "Pull registered devices from Apple Developer Portal for this key's team; push local devices that aren't yet registered"
576
- }))).add(HttpApiEndpoint.get("getCredentials")`/api/apple/asc-api-keys/${idParam$10}/credentials`.addSuccess(AscApiKeyCredentials).annotateContext(OpenApi.annotations({
578
+ }))).add(HttpApiEndpoint.get("getCredentials")`/api/apple/asc-api-keys/${idParam$12}/credentials`.addSuccess(AscApiKeyCredentials).annotateContext(OpenApi.annotations({
577
579
  title: "Get ASC API key credentials",
578
580
  description: "Return the decrypted .p8 PEM, keyId, issuerId, and Apple team for direct App Store Connect API calls from the CLI"
579
581
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -627,7 +629,7 @@ var AssetsGroup = class extends HttpApiGroup.make("assets").add(HttpApiEndpoint.
627
629
 
628
630
  //#endregion
629
631
  //#region ../../packages/api/src/domain/audit-log.ts
630
- const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook");
632
+ const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission");
631
633
  const AuditLogSource = Schema.Literal("session", "api-key");
632
634
  var AuditLog = class extends Schema.Class("AuditLog")({
633
635
  id: Id,
@@ -687,7 +689,7 @@ const DeleteBranchResult = Schema.Struct({ deleted: Schema.Number });
687
689
 
688
690
  //#endregion
689
691
  //#region ../../packages/api/src/groups/branches.ts
690
- const idParam$9 = HttpApiSchema.param("id", Schema.String);
692
+ const idParam$11 = HttpApiSchema.param("id", Schema.String);
691
693
  var BranchesGroup = class extends HttpApiGroup.make("branches").add(HttpApiEndpoint.post("create", "/api/branches").setPayload(CreateBranchBody).addSuccess(Branch, { status: 201 }).annotateContext(OpenApi.annotations({
692
694
  title: "Create branch",
693
695
  description: "Create a new branch within a project"
@@ -699,13 +701,13 @@ var BranchesGroup = class extends HttpApiGroup.make("branches").add(HttpApiEndpo
699
701
  })).annotateContext(OpenApi.annotations({
700
702
  title: "List branches",
701
703
  description: "List all branches for a project"
702
- }))).add(HttpApiEndpoint.get("get")`/api/branches/${idParam$9}`.addSuccess(Branch).annotateContext(OpenApi.annotations({
704
+ }))).add(HttpApiEndpoint.get("get")`/api/branches/${idParam$11}`.addSuccess(Branch).annotateContext(OpenApi.annotations({
703
705
  title: "Get branch",
704
706
  description: "Fetch a single branch by ID"
705
- }))).add(HttpApiEndpoint.patch("rename")`/api/branches/${idParam$9}`.setPayload(UpdateBranchBody).addSuccess(Branch).addError(Conflict).annotateContext(OpenApi.annotations({
707
+ }))).add(HttpApiEndpoint.patch("rename")`/api/branches/${idParam$11}`.setPayload(UpdateBranchBody).addSuccess(Branch).addError(Conflict).annotateContext(OpenApi.annotations({
706
708
  title: "Rename branch",
707
709
  description: "Rename a branch (channels and updates are unaffected)"
708
- }))).add(HttpApiEndpoint.del("delete")`/api/branches/${idParam$9}`.addSuccess(DeleteBranchResult).annotateContext(OpenApi.annotations({
710
+ }))).add(HttpApiEndpoint.del("delete")`/api/branches/${idParam$11}`.addSuccess(DeleteBranchResult).annotateContext(OpenApi.annotations({
709
711
  title: "Delete branch",
710
712
  description: "Delete a branch and all its updates"
711
713
  }))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -779,8 +781,8 @@ const ResolveBuildCredentialsResult = Schema.Union(ResolveBuildCredentialsIosRes
779
781
 
780
782
  //#endregion
781
783
  //#region ../../packages/api/src/groups/build-credentials.ts
782
- const projectIdParam$3 = HttpApiSchema.param("projectId", Schema.String);
783
- var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").add(HttpApiEndpoint.post("resolve")`/api/projects/${projectIdParam$3}/build-credentials/resolve`.setPayload(ResolveBuildCredentialsBody).addSuccess(ResolveBuildCredentialsResult).annotateContext(OpenApi.annotations({
784
+ const projectIdParam$5 = HttpApiSchema.param("projectId", Schema.String);
785
+ var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").add(HttpApiEndpoint.post("resolve")`/api/projects/${projectIdParam$5}/build-credentials/resolve`.setPayload(ResolveBuildCredentialsBody).addSuccess(ResolveBuildCredentialsResult).annotateContext(OpenApi.annotations({
784
786
  title: "Resolve build credentials",
785
787
  description: "Return decrypted signing assets for a project build. Regenerates the iOS provisioning profile via Apple ASC when the registered device roster has changed since the profile was last generated."
786
788
  }))).addError(NotFound).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -940,11 +942,11 @@ const BuildCompatibilityMatrixResult = Schema.Struct({
940
942
 
941
943
  //#endregion
942
944
  //#region ../../packages/api/src/groups/builds.ts
943
- const idParam$8 = HttpApiSchema.param("id", Schema.String);
945
+ const idParam$10 = HttpApiSchema.param("id", Schema.String);
944
946
  var BuildsGroup = class extends HttpApiGroup.make("builds").add(HttpApiEndpoint.post("reserve", "/api/builds").setPayload(CreateBuildBody).addSuccess(ReserveBuildResult, { status: 201 }).annotateContext(OpenApi.annotations({
945
947
  title: "Reserve build",
946
948
  description: "Reserve a build ID and get a presigned upload URL"
947
- }))).add(HttpApiEndpoint.post("complete")`/api/builds/${idParam$8}/complete`.setPayload(CompleteBuildBody).addSuccess(BuildWithArtifact).addError(Conflict).annotateContext(OpenApi.annotations({
949
+ }))).add(HttpApiEndpoint.post("complete")`/api/builds/${idParam$10}/complete`.setPayload(CompleteBuildBody).addSuccess(BuildWithArtifact).addError(Conflict).annotateContext(OpenApi.annotations({
948
950
  title: "Complete build",
949
951
  description: "Finalize a build after artifact upload"
950
952
  }))).add(HttpApiEndpoint.get("list", "/api/builds").setUrlParams(ListBuildsParams).addSuccess(Schema.Struct({
@@ -958,13 +960,13 @@ var BuildsGroup = class extends HttpApiGroup.make("builds").add(HttpApiEndpoint.
958
960
  }))).add(HttpApiEndpoint.get("compatibilityMatrix", "/api/builds/compatibility-matrix").setUrlParams(Schema.Struct({ projectId: Id })).addSuccess(BuildCompatibilityMatrixResult).annotateContext(OpenApi.annotations({
959
961
  title: "Build compatibility matrix",
960
962
  description: "List build-to-channel OTA compatibility for a project"
961
- }))).add(HttpApiEndpoint.get("get")`/api/builds/${idParam$8}`.addSuccess(BuildWithArtifact).annotateContext(OpenApi.annotations({
963
+ }))).add(HttpApiEndpoint.get("get")`/api/builds/${idParam$10}`.addSuccess(BuildWithArtifact).annotateContext(OpenApi.annotations({
962
964
  title: "Get build",
963
965
  description: "Get a build by ID with artifact details"
964
- }))).add(HttpApiEndpoint.del("delete")`/api/builds/${idParam$8}`.addSuccess(DeleteBuildResult).annotateContext(OpenApi.annotations({
966
+ }))).add(HttpApiEndpoint.del("delete")`/api/builds/${idParam$10}`.addSuccess(DeleteBuildResult).annotateContext(OpenApi.annotations({
965
967
  title: "Delete build",
966
968
  description: "Delete a build and its artifact from storage"
967
- }))).add(HttpApiEndpoint.get("getInstallLink")`/api/builds/${idParam$8}/install-link`.addSuccess(InstallLinkResult).annotateContext(OpenApi.annotations({
969
+ }))).add(HttpApiEndpoint.get("getInstallLink")`/api/builds/${idParam$10}/install-link`.addSuccess(InstallLinkResult).annotateContext(OpenApi.annotations({
968
970
  title: "Get install link",
969
971
  description: "Generate a signed install link for a build artifact"
970
972
  }))).addError(NotFound).addError(Forbidden).addError(BadRequest).annotateContext(OpenApi.annotations({
@@ -1009,11 +1011,11 @@ const DeleteChannelResult = Schema.Struct({ deleted: Schema.Number });
1009
1011
 
1010
1012
  //#endregion
1011
1013
  //#region ../../packages/api/src/groups/channels.ts
1012
- const idParam$7 = HttpApiSchema.param("id", Schema.String);
1014
+ const idParam$9 = HttpApiSchema.param("id", Schema.String);
1013
1015
  var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpoint.post("create", "/api/channels").setPayload(CreateChannelBody).addSuccess(Channel, { status: 201 }).annotateContext(OpenApi.annotations({
1014
1016
  title: "Create channel",
1015
1017
  description: "Create a new channel linked to a branch"
1016
- }))).add(HttpApiEndpoint.patch("update")`/api/channels/${idParam$7}`.setPayload(UpdateChannelBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1018
+ }))).add(HttpApiEndpoint.patch("update")`/api/channels/${idParam$9}`.setPayload(UpdateChannelBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1017
1019
  title: "Update channel",
1018
1020
  description: "Relink channel to a different branch"
1019
1021
  }))).add(HttpApiEndpoint.get("list", "/api/channels").setUrlParams(ListChannelsParams).addSuccess(Schema.Struct({
@@ -1024,25 +1026,25 @@ var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpo
1024
1026
  })).annotateContext(OpenApi.annotations({
1025
1027
  title: "List channels",
1026
1028
  description: "List all channels for a project"
1027
- }))).add(HttpApiEndpoint.post("pause")`/api/channels/${idParam$7}/pause`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1029
+ }))).add(HttpApiEndpoint.post("pause")`/api/channels/${idParam$9}/pause`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1028
1030
  title: "Pause channel",
1029
1031
  description: "Pause a channel — manifest requests return 204 No Content"
1030
- }))).add(HttpApiEndpoint.post("resume")`/api/channels/${idParam$7}/resume`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1032
+ }))).add(HttpApiEndpoint.post("resume")`/api/channels/${idParam$9}/resume`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1031
1033
  title: "Resume channel",
1032
1034
  description: "Resume a paused channel"
1033
- }))).add(HttpApiEndpoint.post("createBranchRollout")`/api/channels/${idParam$7}/rollout`.setPayload(CreateBranchRolloutBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1035
+ }))).add(HttpApiEndpoint.post("createBranchRollout")`/api/channels/${idParam$9}/rollout`.setPayload(CreateBranchRolloutBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1034
1036
  title: "Create branch rollout",
1035
1037
  description: "Start a gradual rollout to a new branch on this channel"
1036
- }))).add(HttpApiEndpoint.patch("updateBranchRollout")`/api/channels/${idParam$7}/rollout`.setPayload(UpdateRolloutBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1038
+ }))).add(HttpApiEndpoint.patch("updateBranchRollout")`/api/channels/${idParam$9}/rollout`.setPayload(UpdateRolloutBody).addSuccess(Channel).annotateContext(OpenApi.annotations({
1037
1039
  title: "Update branch rollout",
1038
1040
  description: "Change the rollout percentage for a branch rollout"
1039
- }))).add(HttpApiEndpoint.post("completeBranchRollout")`/api/channels/${idParam$7}/rollout/complete`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1041
+ }))).add(HttpApiEndpoint.post("completeBranchRollout")`/api/channels/${idParam$9}/rollout/complete`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1040
1042
  title: "Complete branch rollout",
1041
1043
  description: "Finalize the rollout — promote the new branch to 100%"
1042
- }))).add(HttpApiEndpoint.post("revertBranchRollout")`/api/channels/${idParam$7}/rollout/revert`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1044
+ }))).add(HttpApiEndpoint.post("revertBranchRollout")`/api/channels/${idParam$9}/rollout/revert`.addSuccess(Channel).annotateContext(OpenApi.annotations({
1043
1045
  title: "Revert branch rollout",
1044
1046
  description: "Revert the rollout — restore the original branch"
1045
- }))).add(HttpApiEndpoint.del("delete")`/api/channels/${idParam$7}`.addSuccess(DeleteChannelResult).annotateContext(OpenApi.annotations({
1047
+ }))).add(HttpApiEndpoint.del("delete")`/api/channels/${idParam$9}`.addSuccess(DeleteChannelResult).annotateContext(OpenApi.annotations({
1046
1048
  title: "Delete channel",
1047
1049
  description: "Delete a channel"
1048
1050
  }))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -1119,7 +1121,7 @@ const ListRegistrationRequestsParams = Schema.Struct({
1119
1121
 
1120
1122
  //#endregion
1121
1123
  //#region ../../packages/api/src/groups/devices.ts
1122
- const idParam$6 = HttpApiSchema.param("id", Schema.String);
1124
+ const idParam$8 = HttpApiSchema.param("id", Schema.String);
1123
1125
  var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoint.post("register", "/api/devices").setPayload(RegisterDeviceBody).addSuccess(Device, { status: 201 }).annotateContext(OpenApi.annotations({
1124
1126
  title: "Register device",
1125
1127
  description: "Register an Apple device UDID in the caller's active organization"
@@ -1131,13 +1133,13 @@ var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoin
1131
1133
  })).annotateContext(OpenApi.annotations({
1132
1134
  title: "List devices",
1133
1135
  description: "List registered Apple devices in the caller's active organization"
1134
- }))).add(HttpApiEndpoint.get("get")`/api/devices/${idParam$6}`.addSuccess(Device).annotateContext(OpenApi.annotations({
1136
+ }))).add(HttpApiEndpoint.get("get")`/api/devices/${idParam$8}`.addSuccess(Device).annotateContext(OpenApi.annotations({
1135
1137
  title: "Get device",
1136
1138
  description: "Get a single device by ID"
1137
- }))).add(HttpApiEndpoint.patch("update")`/api/devices/${idParam$6}`.setPayload(UpdateDeviceBody).addSuccess(Device).annotateContext(OpenApi.annotations({
1139
+ }))).add(HttpApiEndpoint.patch("update")`/api/devices/${idParam$8}`.setPayload(UpdateDeviceBody).addSuccess(Device).annotateContext(OpenApi.annotations({
1138
1140
  title: "Update device",
1139
1141
  description: "Rename a device or toggle its enabled state"
1140
- }))).add(HttpApiEndpoint.del("delete")`/api/devices/${idParam$6}`.addSuccess(DeleteDeviceResult).annotateContext(OpenApi.annotations({
1142
+ }))).add(HttpApiEndpoint.del("delete")`/api/devices/${idParam$8}`.addSuccess(DeleteDeviceResult).annotateContext(OpenApi.annotations({
1141
1143
  title: "Delete device",
1142
1144
  description: "Remove a registered device from the organization"
1143
1145
  }))).add(HttpApiEndpoint.post("createRegistrationRequest", "/api/devices/registration-requests").setPayload(CreateRegistrationRequestBody).addSuccess(DeviceRegistrationRequest, { status: 201 }).annotateContext(OpenApi.annotations({
@@ -1217,7 +1219,7 @@ const EnvVarExportResult = Schema.Struct({
1217
1219
 
1218
1220
  //#endregion
1219
1221
  //#region ../../packages/api/src/groups/env-vars.ts
1220
- const idParam$5 = HttpApiSchema.param("id", Schema.String);
1222
+ const idParam$7 = HttpApiSchema.param("id", Schema.String);
1221
1223
  var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoint.post("create", "/api/env-vars").setPayload(CreateEnvVarBody).addSuccess(EnvVar, { status: 201 }).addError(BadRequest).addError(Conflict).annotateContext(OpenApi.annotations({
1222
1224
  title: "Create environment variable",
1223
1225
  description: "Create a new environment variable. Scope can be 'project' (requires projectId) or 'global' (organization-wide)."
@@ -1230,13 +1232,13 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1230
1232
  })).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).addError(BadRequest).annotateContext(OpenApi.annotations({
1231
1233
  title: "List environment variables",
1232
1234
  description: "List environment variables. scope=all merges project + global vars with project overrides. environments is a comma-separated list. search matches key substring."
1233
- }))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam$5}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
1235
+ }))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam$7}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
1234
1236
  title: "Get environment variable",
1235
1237
  description: "Get an environment variable by ID"
1236
- }))).add(HttpApiEndpoint.patch("update")`/api/env-vars/${idParam$5}`.setPayload(UpdateEnvVarBody).addSuccess(EnvVar).addError(BadRequest).annotateContext(OpenApi.annotations({
1238
+ }))).add(HttpApiEndpoint.patch("update")`/api/env-vars/${idParam$7}`.setPayload(UpdateEnvVarBody).addSuccess(EnvVar).addError(BadRequest).annotateContext(OpenApi.annotations({
1237
1239
  title: "Update environment variable",
1238
1240
  description: "Update value, visibility, or assigned environments"
1239
- }))).add(HttpApiEndpoint.del("delete")`/api/env-vars/${idParam$5}`.addSuccess(DeleteEnvVarResult).annotateContext(OpenApi.annotations({
1241
+ }))).add(HttpApiEndpoint.del("delete")`/api/env-vars/${idParam$7}`.addSuccess(DeleteEnvVarResult).annotateContext(OpenApi.annotations({
1240
1242
  title: "Delete environment variable",
1241
1243
  description: "Delete an environment variable"
1242
1244
  }))).add(HttpApiEndpoint.post("bulkImport", "/api/env-vars/bulk-import").setPayload(BulkImportEnvVarsBody).addSuccess(BulkImportResult).addError(BadRequest).annotateContext(OpenApi.annotations({
@@ -1342,7 +1344,7 @@ const DeleteUpdateResult = Schema.Struct({ deleted: Schema.Number });
1342
1344
 
1343
1345
  //#endregion
1344
1346
  //#region ../../packages/api/src/groups/fingerprints.ts
1345
- const projectIdParam$2 = HttpApiSchema.param("projectId", Id);
1347
+ const projectIdParam$4 = HttpApiSchema.param("projectId", Id);
1346
1348
  const hashParam = HttpApiSchema.param("hash", Schema.String.pipe(Schema.minLength(1)));
1347
1349
  const FingerprintDetail = Schema.Struct({
1348
1350
  hash: Schema.String,
@@ -1350,7 +1352,7 @@ const FingerprintDetail = Schema.Struct({
1350
1352
  builds: Schema.Array(BuildWithArtifact),
1351
1353
  updates: Schema.Array(Update)
1352
1354
  });
1353
- var FingerprintsGroup = class extends HttpApiGroup.make("fingerprints").add(HttpApiEndpoint.get("get")`/api/projects/${projectIdParam$2}/fingerprints/${hashParam}`.addSuccess(FingerprintDetail).annotateContext(OpenApi.annotations({
1355
+ var FingerprintsGroup = class extends HttpApiGroup.make("fingerprints").add(HttpApiEndpoint.get("get")`/api/projects/${projectIdParam$4}/fingerprints/${hashParam}`.addSuccess(FingerprintDetail).annotateContext(OpenApi.annotations({
1354
1356
  title: "Get fingerprint",
1355
1357
  description: "Fetch builds and updates compatible with a given fingerprint hash within a project."
1356
1358
  }))).addError(Forbidden, { status: 403 }).addError(NotFound, { status: 404 }).addError(BadRequest, { status: 400 }) {};
@@ -1376,17 +1378,17 @@ const DownloadGoogleServiceAccountKeyResult = Schema.Struct({
1376
1378
 
1377
1379
  //#endregion
1378
1380
  //#region ../../packages/api/src/groups/google-service-account-keys.ts
1379
- const idParam$4 = HttpApiSchema.param("id", Schema.String);
1381
+ const idParam$6 = HttpApiSchema.param("id", Schema.String);
1380
1382
  var GoogleServiceAccountKeysGroup = class extends HttpApiGroup.make("googleServiceAccountKeys").add(HttpApiEndpoint.get("list", "/api/google/service-account-keys").addSuccess(Schema.Struct({ items: Schema.Array(GoogleServiceAccountKey) })).annotateContext(OpenApi.annotations({
1381
1383
  title: "List Google service account keys",
1382
1384
  description: "List uploaded Google service account JSON keys"
1383
1385
  }))).add(HttpApiEndpoint.post("upload", "/api/google/service-account-keys").setPayload(UploadGoogleServiceAccountKeyBody).addSuccess(GoogleServiceAccountKey, { status: 201 }).annotateContext(OpenApi.annotations({
1384
1386
  title: "Upload service account key",
1385
1387
  description: "Upload a Google service account JSON key"
1386
- }))).add(HttpApiEndpoint.del("delete")`/api/google/service-account-keys/${idParam$4}`.addSuccess(DeleteGoogleServiceAccountKeyResult).annotateContext(OpenApi.annotations({
1388
+ }))).add(HttpApiEndpoint.del("delete")`/api/google/service-account-keys/${idParam$6}`.addSuccess(DeleteGoogleServiceAccountKeyResult).annotateContext(OpenApi.annotations({
1387
1389
  title: "Delete service account key",
1388
1390
  description: "Remove a stored Google service account key"
1389
- }))).add(HttpApiEndpoint.get("download")`/api/google/service-account-keys/${idParam$4}/download`.addSuccess(DownloadGoogleServiceAccountKeyResult).annotateContext(OpenApi.annotations({
1391
+ }))).add(HttpApiEndpoint.get("download")`/api/google/service-account-keys/${idParam$6}/download`.addSuccess(DownloadGoogleServiceAccountKeyResult).annotateContext(OpenApi.annotations({
1390
1392
  title: "Download service account key",
1391
1393
  description: "Fetch the decrypted JSON for local use (audit-logged)"
1392
1394
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -1394,8 +1396,67 @@ var GoogleServiceAccountKeysGroup = class extends HttpApiGroup.make("googleServi
1394
1396
  description: "Manage Google Play + FCM service account JSON keys"
1395
1397
  })) {};
1396
1398
 
1399
+ //#endregion
1400
+ //#region ../../packages/api/src/domain/ios-app-metadata.ts
1401
+ const AscAppId = Schema.String.pipe(Schema.pattern(/^[0-9]{1,30}$/u, { message: () => "ASC App ID must be 1-30 digits" }));
1402
+ const AppStoreLanguage = Schema.String.pipe(Schema.minLength(2), Schema.maxLength(10));
1403
+ const AppStoreSku = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100));
1404
+ const CompanyName = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200));
1405
+ const AppName = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200));
1406
+ var IosAppMetadata = class extends Schema.Class("IosAppMetadata")({
1407
+ id: Id,
1408
+ organizationId: Id,
1409
+ projectId: Id,
1410
+ bundleIdentifier: Schema.String,
1411
+ ascAppId: Schema.NullOr(Schema.String),
1412
+ sku: Schema.NullOr(Schema.String),
1413
+ language: Schema.String,
1414
+ companyName: Schema.NullOr(Schema.String),
1415
+ appName: Schema.NullOr(Schema.String),
1416
+ createdAt: DateTimeString,
1417
+ updatedAt: DateTimeString
1418
+ }) {};
1419
+ const CreateIosAppMetadataBody = Schema.Struct({
1420
+ bundleIdentifier: BundleIdentifier,
1421
+ ascAppId: Schema.optional(AscAppId),
1422
+ sku: Schema.optional(AppStoreSku),
1423
+ language: Schema.optional(AppStoreLanguage),
1424
+ companyName: Schema.optional(CompanyName),
1425
+ appName: Schema.optional(AppName)
1426
+ });
1427
+ const UpdateIosAppMetadataBody = Schema.Struct({
1428
+ ascAppId: Schema.optional(Schema.NullOr(AscAppId)),
1429
+ sku: Schema.optional(Schema.NullOr(AppStoreSku)),
1430
+ language: Schema.optional(AppStoreLanguage),
1431
+ companyName: Schema.optional(Schema.NullOr(CompanyName)),
1432
+ appName: Schema.optional(Schema.NullOr(AppName))
1433
+ });
1434
+ const DeleteIosAppMetadataResult = Schema.Struct({ deleted: Schema.Number });
1435
+
1436
+ //#endregion
1437
+ //#region ../../packages/api/src/groups/ios-app-metadata.ts
1438
+ const idParam$5 = HttpApiSchema.param("id", Schema.String);
1439
+ const projectIdParam$3 = HttpApiSchema.param("projectId", Schema.String);
1440
+ var IosAppMetadataGroup = class extends HttpApiGroup.make("iosAppMetadata").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$3}/ios-app-metadata`.addSuccess(Schema.Struct({ items: Schema.Array(IosAppMetadata) })).annotateContext(OpenApi.annotations({
1441
+ title: "List iOS App Store metadata",
1442
+ description: "List App Store Connect metadata entries for a project"
1443
+ }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$3}/ios-app-metadata`.setPayload(CreateIosAppMetadataBody).addSuccess(IosAppMetadata, { status: 201 }).annotateContext(OpenApi.annotations({
1444
+ title: "Create iOS App Store metadata",
1445
+ description: "Register App Store Connect metadata for a bundle identifier"
1446
+ }))).add(HttpApiEndpoint.put("update")`/api/ios-app-metadata/${idParam$5}`.setPayload(UpdateIosAppMetadataBody).addSuccess(IosAppMetadata).annotateContext(OpenApi.annotations({
1447
+ title: "Update iOS App Store metadata",
1448
+ description: "Change ASC app id / sku / language / company name / app name"
1449
+ }))).add(HttpApiEndpoint.del("delete")`/api/ios-app-metadata/${idParam$5}`.addSuccess(DeleteIosAppMetadataResult).annotateContext(OpenApi.annotations({
1450
+ title: "Delete iOS App Store metadata",
1451
+ description: "Remove an App Store metadata entry"
1452
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
1453
+ title: "iOS App Store metadata",
1454
+ description: "Per-project per-bundle App Store Connect metadata for submissions"
1455
+ })) {};
1456
+
1397
1457
  //#endregion
1398
1458
  //#region ../../packages/api/src/domain/ios-bundle-configuration.ts
1459
+ const TargetName = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(200));
1399
1460
  var IosBundleConfiguration = class extends Schema.Class("IosBundleConfiguration")({
1400
1461
  id: Id,
1401
1462
  organizationId: Id,
@@ -1407,6 +1468,8 @@ var IosBundleConfiguration = class extends Schema.Class("IosBundleConfiguration"
1407
1468
  appleProvisioningProfileId: Schema.NullOr(Id),
1408
1469
  applePushKeyId: Schema.NullOr(Id),
1409
1470
  ascApiKeyId: Schema.NullOr(Id),
1471
+ targetName: Schema.NullOr(Schema.String),
1472
+ parentBundleIdentifier: Schema.NullOr(Schema.String),
1410
1473
  createdAt: DateTimeString,
1411
1474
  updatedAt: DateTimeString
1412
1475
  }) {};
@@ -1417,30 +1480,34 @@ const CreateIosBundleConfigurationBody = Schema.Struct({
1417
1480
  appleDistributionCertificateId: Schema.optional(Id),
1418
1481
  appleProvisioningProfileId: Schema.optional(Id),
1419
1482
  applePushKeyId: Schema.optional(Id),
1420
- ascApiKeyId: Schema.optional(Id)
1483
+ ascApiKeyId: Schema.optional(Id),
1484
+ targetName: Schema.optional(TargetName),
1485
+ parentBundleIdentifier: Schema.optional(BundleIdentifier)
1421
1486
  });
1422
1487
  const UpdateIosBundleConfigurationBody = Schema.Struct({
1423
1488
  appleDistributionCertificateId: Schema.optional(Schema.NullOr(Id)),
1424
1489
  appleProvisioningProfileId: Schema.optional(Schema.NullOr(Id)),
1425
1490
  applePushKeyId: Schema.optional(Schema.NullOr(Id)),
1426
- ascApiKeyId: Schema.optional(Schema.NullOr(Id))
1491
+ ascApiKeyId: Schema.optional(Schema.NullOr(Id)),
1492
+ targetName: Schema.optional(Schema.NullOr(TargetName)),
1493
+ parentBundleIdentifier: Schema.optional(Schema.NullOr(BundleIdentifier))
1427
1494
  });
1428
1495
  const DeleteIosBundleConfigurationResult = Schema.Struct({ deleted: Schema.Number });
1429
1496
 
1430
1497
  //#endregion
1431
1498
  //#region ../../packages/api/src/groups/ios-bundle-configurations.ts
1432
- const idParam$3 = HttpApiSchema.param("id", Schema.String);
1433
- const projectIdParam$1 = HttpApiSchema.param("projectId", Schema.String);
1434
- var IosBundleConfigurationsGroup = class extends HttpApiGroup.make("iosBundleConfigurations").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$1}/ios-bundle-configurations`.addSuccess(Schema.Struct({ items: Schema.Array(IosBundleConfiguration) })).annotateContext(OpenApi.annotations({
1499
+ const idParam$4 = HttpApiSchema.param("id", Schema.String);
1500
+ const projectIdParam$2 = HttpApiSchema.param("projectId", Schema.String);
1501
+ var IosBundleConfigurationsGroup = class extends HttpApiGroup.make("iosBundleConfigurations").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$2}/ios-bundle-configurations`.addSuccess(Schema.Struct({ items: Schema.Array(IosBundleConfiguration) })).annotateContext(OpenApi.annotations({
1435
1502
  title: "List iOS bundle configurations",
1436
1503
  description: "List all iOS bundle configurations for a project"
1437
- }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$1}/ios-bundle-configurations`.setPayload(CreateIosBundleConfigurationBody).addSuccess(IosBundleConfiguration, { status: 201 }).annotateContext(OpenApi.annotations({
1504
+ }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$2}/ios-bundle-configurations`.setPayload(CreateIosBundleConfigurationBody).addSuccess(IosBundleConfiguration, { status: 201 }).annotateContext(OpenApi.annotations({
1438
1505
  title: "Create iOS bundle configuration",
1439
1506
  description: "Bind certificate + profile + push + ASC to a bundle identifier"
1440
- }))).add(HttpApiEndpoint.put("update")`/api/ios-bundle-configurations/${idParam$3}`.setPayload(UpdateIosBundleConfigurationBody).addSuccess(IosBundleConfiguration).annotateContext(OpenApi.annotations({
1507
+ }))).add(HttpApiEndpoint.put("update")`/api/ios-bundle-configurations/${idParam$4}`.setPayload(UpdateIosBundleConfigurationBody).addSuccess(IosBundleConfiguration).annotateContext(OpenApi.annotations({
1441
1508
  title: "Update iOS bundle configuration",
1442
1509
  description: "Change the credentials bound to an iOS bundle configuration"
1443
- }))).add(HttpApiEndpoint.del("delete")`/api/ios-bundle-configurations/${idParam$3}`.addSuccess(DeleteIosBundleConfigurationResult).annotateContext(OpenApi.annotations({
1510
+ }))).add(HttpApiEndpoint.del("delete")`/api/ios-bundle-configurations/${idParam$4}`.addSuccess(DeleteIosBundleConfigurationResult).annotateContext(OpenApi.annotations({
1444
1511
  title: "Delete iOS bundle configuration",
1445
1512
  description: "Remove an iOS bundle configuration binding"
1446
1513
  }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -1513,7 +1580,7 @@ const DeleteProjectResult = Schema.Struct({ deleted: Schema.Number });
1513
1580
 
1514
1581
  //#endregion
1515
1582
  //#region ../../packages/api/src/groups/projects.ts
1516
- const idParam$2 = HttpApiSchema.param("id", Schema.String);
1583
+ const idParam$3 = HttpApiSchema.param("id", Schema.String);
1517
1584
  const slugParam = HttpApiSchema.param("slug", Schema.String);
1518
1585
  var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpoint.post("create", "/api/projects").setPayload(CreateProjectBody).addSuccess(Project, { status: 201 }).annotateContext(OpenApi.annotations({
1519
1586
  title: "Create project",
@@ -1526,16 +1593,16 @@ var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpo
1526
1593
  })).annotateContext(OpenApi.annotations({
1527
1594
  title: "List projects",
1528
1595
  description: "List all projects in the caller's active organization"
1529
- }))).add(HttpApiEndpoint.get("get")`/api/projects/${idParam$2}`.addSuccess(Project).annotateContext(OpenApi.annotations({
1596
+ }))).add(HttpApiEndpoint.get("get")`/api/projects/${idParam$3}`.addSuccess(Project).annotateContext(OpenApi.annotations({
1530
1597
  title: "Get project",
1531
1598
  description: "Get a single project by ID"
1532
1599
  }))).add(HttpApiEndpoint.get("getBySlug")`/api/projects/by-slug/${slugParam}`.addSuccess(Project).annotateContext(OpenApi.annotations({
1533
1600
  title: "Get project by slug",
1534
1601
  description: "Get a single project by slug within the caller's active organization"
1535
- }))).add(HttpApiEndpoint.patch("rename")`/api/projects/${idParam$2}`.setPayload(UpdateProjectBody).addSuccess(Project).annotateContext(OpenApi.annotations({
1602
+ }))).add(HttpApiEndpoint.patch("rename")`/api/projects/${idParam$3}`.setPayload(UpdateProjectBody).addSuccess(Project).annotateContext(OpenApi.annotations({
1536
1603
  title: "Rename project",
1537
1604
  description: "Rename a project"
1538
- }))).add(HttpApiEndpoint.del("delete")`/api/projects/${idParam$2}`.addSuccess(DeleteProjectResult).annotateContext(OpenApi.annotations({
1605
+ }))).add(HttpApiEndpoint.del("delete")`/api/projects/${idParam$3}`.addSuccess(DeleteProjectResult).annotateContext(OpenApi.annotations({
1539
1606
  title: "Delete project",
1540
1607
  description: "Delete a project and all its branches, channels, and updates"
1541
1608
  }))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -1543,6 +1610,128 @@ var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpo
1543
1610
  description: "Project management endpoints"
1544
1611
  })) {};
1545
1612
 
1613
+ //#endregion
1614
+ //#region ../../packages/api/src/domain/submission.ts
1615
+ const SubmissionStatus = Schema.Literal("AWAITING_BUILD", "IN_QUEUE", "IN_PROGRESS", "FINISHED", "ERRORED", "CANCELED");
1616
+ const SubmissionArchiveSource = Schema.Literal("build", "path", "url");
1617
+ const AndroidReleaseStatus = Schema.Literal("completed", "draft", "halted", "inProgress");
1618
+ const AndroidTrack = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100));
1619
+ const Rollout = Schema.Number.pipe(Schema.greaterThan(0), Schema.lessThanOrEqualTo(1));
1620
+ var IosSubmissionConfig = class extends Schema.Class("IosSubmissionConfig")({
1621
+ appleId: Schema.NullOr(Schema.String),
1622
+ ascAppId: Schema.NullOr(Schema.String),
1623
+ appleTeamId: Schema.NullOr(Schema.String),
1624
+ sku: Schema.NullOr(Schema.String),
1625
+ language: Schema.String,
1626
+ companyName: Schema.NullOr(Schema.String),
1627
+ appName: Schema.NullOr(Schema.String),
1628
+ bundleIdentifier: Schema.String,
1629
+ ascApiKeyId: Schema.NullOr(Id),
1630
+ groups: Schema.Array(Schema.String),
1631
+ whatToTest: Schema.NullOr(Schema.String)
1632
+ }) {};
1633
+ var AndroidSubmissionConfig = class extends Schema.Class("AndroidSubmissionConfig")({
1634
+ applicationId: Schema.String,
1635
+ track: Schema.String,
1636
+ releaseStatus: AndroidReleaseStatus,
1637
+ changesNotSentForReview: Schema.Boolean,
1638
+ rollout: Schema.NullOr(Schema.Number),
1639
+ googleServiceAccountKeyId: Schema.NullOr(Id)
1640
+ }) {};
1641
+ var Submission = class extends Schema.Class("Submission")({
1642
+ id: Id,
1643
+ organizationId: Id,
1644
+ projectId: Id,
1645
+ platform: Platform,
1646
+ profileName: Schema.String,
1647
+ status: SubmissionStatus,
1648
+ archiveSource: SubmissionArchiveSource,
1649
+ buildId: Schema.NullOr(Id),
1650
+ archiveUrl: Schema.NullOr(Schema.String),
1651
+ iosConfig: Schema.NullOr(IosSubmissionConfig),
1652
+ androidConfig: Schema.NullOr(AndroidSubmissionConfig),
1653
+ errorCode: Schema.NullOr(Schema.String),
1654
+ errorMessage: Schema.NullOr(Schema.String),
1655
+ logFiles: Schema.Array(Schema.String),
1656
+ initiatingUserId: Schema.NullOr(Schema.String),
1657
+ queuedAt: Schema.NullOr(DateTimeString),
1658
+ startedAt: Schema.NullOr(DateTimeString),
1659
+ completedAt: Schema.NullOr(DateTimeString),
1660
+ createdAt: DateTimeString,
1661
+ updatedAt: DateTimeString
1662
+ }) {};
1663
+ const CreateIosSubmissionBody = Schema.Struct({
1664
+ appleId: Schema.optional(Schema.String),
1665
+ ascAppId: Schema.optional(Schema.String),
1666
+ appleTeamId: Schema.optional(Schema.String),
1667
+ sku: Schema.optional(Schema.String),
1668
+ language: Schema.optional(Schema.String),
1669
+ companyName: Schema.optional(Schema.String),
1670
+ appName: Schema.optional(Schema.String),
1671
+ bundleIdentifier: BundleIdentifier,
1672
+ ascApiKeyId: Schema.optional(Id),
1673
+ groups: Schema.optional(Schema.Array(Schema.String)),
1674
+ whatToTest: Schema.optional(Schema.String)
1675
+ });
1676
+ const CreateAndroidSubmissionBody = Schema.Struct({
1677
+ applicationId: Schema.String,
1678
+ track: Schema.optional(AndroidTrack),
1679
+ releaseStatus: Schema.optional(AndroidReleaseStatus),
1680
+ changesNotSentForReview: Schema.optional(Schema.Boolean),
1681
+ rollout: Schema.optional(Rollout),
1682
+ googleServiceAccountKeyId: Schema.optional(Id)
1683
+ });
1684
+ const CreateSubmissionBody = Schema.Struct({
1685
+ platform: Platform,
1686
+ profileName: Schema.optional(Schema.String),
1687
+ archiveSource: SubmissionArchiveSource,
1688
+ buildId: Schema.optional(Id),
1689
+ archiveUrl: Schema.optional(Schema.String),
1690
+ iosConfig: Schema.optional(CreateIosSubmissionBody),
1691
+ androidConfig: Schema.optional(CreateAndroidSubmissionBody)
1692
+ });
1693
+ const UpdateSubmissionStatusBody = Schema.Struct({
1694
+ status: SubmissionStatus,
1695
+ errorCode: Schema.optional(Schema.NullOr(Schema.String)),
1696
+ errorMessage: Schema.optional(Schema.NullOr(Schema.String)),
1697
+ logFiles: Schema.optional(Schema.Array(Schema.String))
1698
+ });
1699
+ const CancelSubmissionResult = Schema.Struct({ canceled: Schema.Boolean });
1700
+ const DeleteSubmissionResult = Schema.Struct({ deleted: Schema.Number });
1701
+
1702
+ //#endregion
1703
+ //#region ../../packages/api/src/groups/submissions.ts
1704
+ const idParam$2 = HttpApiSchema.param("id", Schema.String);
1705
+ const projectIdParam$1 = HttpApiSchema.param("projectId", Schema.String);
1706
+ const ListParams = Schema.Struct({
1707
+ status: Schema.optional(SubmissionStatus),
1708
+ platform: Schema.optional(Platform),
1709
+ profile: Schema.optional(Schema.String),
1710
+ buildId: Schema.optional(Schema.String)
1711
+ });
1712
+ var SubmissionsGroup = class extends HttpApiGroup.make("submissions").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$1}/submissions`.setUrlParams(ListParams).addSuccess(Schema.Struct({ items: Schema.Array(Submission) })).annotateContext(OpenApi.annotations({
1713
+ title: "List submissions",
1714
+ description: "List store submissions for a project with optional filters"
1715
+ }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$1}/submissions`.setPayload(CreateSubmissionBody).addSuccess(Submission, { status: 201 }).annotateContext(OpenApi.annotations({
1716
+ title: "Create submission",
1717
+ description: "Start a store submission for a build / IPA / AAB"
1718
+ }))).add(HttpApiEndpoint.get("get")`/api/submissions/${idParam$2}`.addSuccess(Submission).annotateContext(OpenApi.annotations({
1719
+ title: "Get submission",
1720
+ description: "Get a submission by id"
1721
+ }))).add(HttpApiEndpoint.patch("updateStatus")`/api/submissions/${idParam$2}/status`.setPayload(UpdateSubmissionStatusBody).addSuccess(Submission).annotateContext(OpenApi.annotations({
1722
+ title: "Update submission status",
1723
+ description: "Patch status / error / logFiles. CLI uses this for iOS submissions."
1724
+ }))).add(HttpApiEndpoint.post("cancel")`/api/submissions/${idParam$2}/cancel`.addSuccess(CancelSubmissionResult).annotateContext(OpenApi.annotations({
1725
+ title: "Cancel submission",
1726
+ description: "Cancel an AWAITING_BUILD / IN_QUEUE submission"
1727
+ }))).add(HttpApiEndpoint.del("delete")`/api/submissions/${idParam$2}`.addSuccess(DeleteSubmissionResult).annotateContext(OpenApi.annotations({
1728
+ title: "Delete submission",
1729
+ description: "Delete a terminal submission record"
1730
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
1731
+ title: "Submissions",
1732
+ description: "Store-submission lifecycle (App Store + Google Play)"
1733
+ })) {};
1734
+
1546
1735
  //#endregion
1547
1736
  //#region ../../packages/api/src/groups/updates.ts
1548
1737
  const idParam$1 = HttpApiSchema.param("id", Schema.String);
@@ -1644,7 +1833,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
1644
1833
 
1645
1834
  //#endregion
1646
1835
  //#region ../../packages/api/src/api.ts
1647
- var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(MeGroup).add(WebhooksGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
1836
+ var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).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(MeGroup).add(WebhooksGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
1648
1837
  title: "Better Update Management API",
1649
1838
  version: "1.0.0",
1650
1839
  description: "Management API for OTA update publishing, deployment, and analytics"
@@ -3412,7 +3601,7 @@ const pemToPkcs8Der = (pem) => {
3412
3601
  //#region src/lib/apple-asc-jwt.ts
3413
3602
  var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
3414
3603
  const MAX_JWT_LIFETIME_SECONDS = 1200;
3415
- const asArrayBuffer = (bytes) => {
3604
+ const asArrayBuffer$1 = (bytes) => {
3416
3605
  const buffer = new ArrayBuffer(bytes.byteLength);
3417
3606
  new Uint8Array(buffer).set(bytes);
3418
3607
  return buffer;
@@ -3434,7 +3623,7 @@ const signAscJwt = (credentials) => Effect.gen(function* () {
3434
3623
  };
3435
3624
  const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
3436
3625
  const key = yield* Effect.tryPromise({
3437
- try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
3626
+ try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer$1(der), {
3438
3627
  name: "ECDSA",
3439
3628
  namedCurve: "P-256"
3440
3629
  }, false, ["sign"]),
@@ -4653,12 +4842,6 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4653
4842
  const buildType = androidProfile.buildType ?? "release";
4654
4843
  const androidDir = path.join(projectRoot, "android");
4655
4844
  const commandEnv = yield* runtime.commandEnvironment(envVars);
4656
- const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
4657
- projectId,
4658
- applicationIdentifier,
4659
- tempDir,
4660
- buildProfile: input.profileName
4661
- });
4662
4845
  yield* runStep({
4663
4846
  command: "bunx",
4664
4847
  args: [
@@ -4671,21 +4854,27 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4671
4854
  cwd: projectRoot,
4672
4855
  env: commandEnv
4673
4856
  }, "expo prebuild android");
4674
- const fs = yield* FileSystem.FileSystem;
4675
- const signingGradlePath = path.join(tempDir, "signing.gradle");
4676
- yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
4677
- keystorePath: credentials.keystorePath,
4678
- storePassword: credentials.storePassword,
4679
- keyAlias: credentials.keyAlias,
4680
- keyPassword: credentials.keyPassword
4681
- }));
4857
+ const gradleArgs = yield* input.skipCredentials ? Effect.succeed([]) : Effect.gen(function* () {
4858
+ const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
4859
+ projectId,
4860
+ applicationIdentifier,
4861
+ tempDir,
4862
+ buildProfile: input.profileName
4863
+ });
4864
+ const fs = yield* FileSystem.FileSystem;
4865
+ const signingGradlePath = path.join(tempDir, "signing.gradle");
4866
+ yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
4867
+ keystorePath: credentials.keystorePath,
4868
+ storePassword: credentials.storePassword,
4869
+ keyAlias: credentials.keyAlias,
4870
+ keyPassword: credentials.keyPassword
4871
+ }));
4872
+ return ["--init-script", signingGradlePath];
4873
+ });
4874
+ const taskName = gradleTaskName(format, flavor, buildType);
4682
4875
  yield* runStep({
4683
4876
  command: "./gradlew",
4684
- args: [
4685
- "--init-script",
4686
- signingGradlePath,
4687
- `:app:${gradleTaskName(format, flavor, buildType)}`
4688
- ],
4877
+ args: [...gradleArgs, `:app:${taskName}`],
4689
4878
  cwd: androidDir,
4690
4879
  env: commandEnv
4691
4880
  }, "gradlew");
@@ -6119,6 +6308,137 @@ const applyAutoIncrement = (input) => Effect.gen(function* () {
6119
6308
  return bumps;
6120
6309
  });
6121
6310
 
6311
+ //#endregion
6312
+ //#region src/lib/eas-submit-config.ts
6313
+ const MAX_SUBMIT_EXTENDS_DEPTH = 10;
6314
+ const asStringValue$1 = (value) => typeof value === "string" ? value : void 0;
6315
+ const asBooleanValue$1 = (value) => typeof value === "boolean" ? value : void 0;
6316
+ const asNumberValue = (raw) => typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
6317
+ const asStringArray = (raw) => {
6318
+ if (!Array.isArray(raw)) return;
6319
+ const items = raw.filter((item) => typeof item === "string");
6320
+ return items.length === 0 ? void 0 : items;
6321
+ };
6322
+ const asAndroidReleaseStatus = (raw) => {
6323
+ const value = asStringValue$1(raw);
6324
+ return value === "completed" || value === "draft" || value === "halted" || value === "inProgress" ? value : void 0;
6325
+ };
6326
+ const parseIosSubmitProfile = (raw) => {
6327
+ const record = asRecord(raw);
6328
+ if (!record) return;
6329
+ const appleId = asStringValue$1(record["appleId"]);
6330
+ const ascAppId = asStringValue$1(record["ascAppId"]);
6331
+ const appleTeamId = asStringValue$1(record["appleTeamId"]);
6332
+ const ascApiKeyPath = asStringValue$1(record["ascApiKeyPath"]);
6333
+ const ascApiKeyId = asStringValue$1(record["ascApiKeyId"]);
6334
+ const ascApiKeyIssuerId = asStringValue$1(record["ascApiKeyIssuerId"]);
6335
+ const sku = asStringValue$1(record["sku"]);
6336
+ const language = asStringValue$1(record["language"]);
6337
+ const companyName = asStringValue$1(record["companyName"]);
6338
+ const appName = asStringValue$1(record["appName"]);
6339
+ const bundleIdentifier = asStringValue$1(record["bundleIdentifier"]);
6340
+ const metadataPath = asStringValue$1(record["metadataPath"]);
6341
+ const groups = asStringArray(record["groups"]);
6342
+ return {
6343
+ ...appleId === void 0 ? {} : { appleId },
6344
+ ...ascAppId === void 0 ? {} : { ascAppId },
6345
+ ...appleTeamId === void 0 ? {} : { appleTeamId },
6346
+ ...ascApiKeyPath === void 0 ? {} : { ascApiKeyPath },
6347
+ ...ascApiKeyId === void 0 ? {} : { ascApiKeyId },
6348
+ ...ascApiKeyIssuerId === void 0 ? {} : { ascApiKeyIssuerId },
6349
+ ...sku === void 0 ? {} : { sku },
6350
+ ...language === void 0 ? {} : { language },
6351
+ ...companyName === void 0 ? {} : { companyName },
6352
+ ...appName === void 0 ? {} : { appName },
6353
+ ...bundleIdentifier === void 0 ? {} : { bundleIdentifier },
6354
+ ...metadataPath === void 0 ? {} : { metadataPath },
6355
+ ...groups === void 0 ? {} : { groups }
6356
+ };
6357
+ };
6358
+ const parseAndroidSubmitProfile = (raw) => {
6359
+ const record = asRecord(raw);
6360
+ if (!record) return;
6361
+ const serviceAccountKeyPath = asStringValue$1(record["serviceAccountKeyPath"]);
6362
+ const serviceAccountKeyId = asStringValue$1(record["serviceAccountKeyId"]);
6363
+ const track = asStringValue$1(record["track"]);
6364
+ const releaseStatus = asAndroidReleaseStatus(record["releaseStatus"]);
6365
+ const changesNotSentForReview = asBooleanValue$1(record["changesNotSentForReview"]);
6366
+ const rollout = asNumberValue(record["rollout"]);
6367
+ const applicationId = asStringValue$1(record["applicationId"]);
6368
+ return {
6369
+ ...serviceAccountKeyPath === void 0 ? {} : { serviceAccountKeyPath },
6370
+ ...serviceAccountKeyId === void 0 ? {} : { serviceAccountKeyId },
6371
+ ...track === void 0 ? {} : { track },
6372
+ ...releaseStatus === void 0 ? {} : { releaseStatus },
6373
+ ...changesNotSentForReview === void 0 ? {} : { changesNotSentForReview },
6374
+ ...rollout === void 0 ? {} : { rollout },
6375
+ ...applicationId === void 0 ? {} : { applicationId }
6376
+ };
6377
+ };
6378
+ const parseSubmitProfile = (raw) => {
6379
+ const record = asRecord(raw);
6380
+ if (!record) return;
6381
+ const extendsName = asStringValue$1(record["extends"]);
6382
+ const ios = parseIosSubmitProfile(record["ios"]);
6383
+ const android = parseAndroidSubmitProfile(record["android"]);
6384
+ return {
6385
+ ...extendsName === void 0 ? {} : { extends: extendsName },
6386
+ ...ios === void 0 ? {} : { ios },
6387
+ ...android === void 0 ? {} : { android }
6388
+ };
6389
+ };
6390
+ const mergeIosSubmit = (base, overlay) => {
6391
+ if (!base) return overlay;
6392
+ if (!overlay) return base;
6393
+ return {
6394
+ ...base,
6395
+ ...overlay
6396
+ };
6397
+ };
6398
+ const mergeAndroidSubmit = (base, overlay) => {
6399
+ if (!base) return overlay;
6400
+ if (!overlay) return base;
6401
+ return {
6402
+ ...base,
6403
+ ...overlay
6404
+ };
6405
+ };
6406
+ const mergeSubmitProfile = (base, overlay) => {
6407
+ const ios = mergeIosSubmit(base.ios, overlay.ios);
6408
+ const android = mergeAndroidSubmit(base.android, overlay.android);
6409
+ return {
6410
+ ...overlay.extends === void 0 ? {} : { extends: overlay.extends },
6411
+ ...ios === void 0 ? {} : { ios },
6412
+ ...android === void 0 ? {} : { android }
6413
+ };
6414
+ };
6415
+ const collectSubmitExtendsChain = (profiles, profileName) => Effect.gen(function* () {
6416
+ const chain = [];
6417
+ const visited = /* @__PURE__ */ new Set();
6418
+ let current = profileName;
6419
+ let depth = 0;
6420
+ while (current !== void 0) {
6421
+ if (visited.has(current)) return yield* new BuildProfileError({ message: `Cycle detected in eas.json submit.${profileName} extends chain at "${current}".` });
6422
+ visited.add(current);
6423
+ const profile = profiles[current];
6424
+ if (!profile) return yield* new BuildProfileError({ message: current === profileName ? `Submit profile "${profileName}" not found in eas.json.` : `Submit profile "${profileName}" extends missing profile "${current}".` });
6425
+ chain.unshift(profile);
6426
+ current = profile.extends;
6427
+ depth += 1;
6428
+ if (depth > MAX_SUBMIT_EXTENDS_DEPTH) return yield* new BuildProfileError({ message: `Too many "extends" levels (max ${String(MAX_SUBMIT_EXTENDS_DEPTH)}) in eas.json submit.${profileName}.` });
6429
+ }
6430
+ return chain;
6431
+ });
6432
+ const stripSubmitExtends = (profile) => {
6433
+ if (profile.extends === void 0) return profile;
6434
+ const { extends: _omit, ...rest } = profile;
6435
+ return rest;
6436
+ };
6437
+ const resolveEasSubmitProfile = (profiles, profileName) => Effect.gen(function* () {
6438
+ if (!profiles) return yield* new BuildProfileError({ message: "eas.json has no \"submit\" section. Add at least one submit profile." });
6439
+ return stripSubmitExtends((yield* collectSubmitExtendsChain(profiles, profileName)).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
6440
+ });
6441
+
6122
6442
  //#endregion
6123
6443
  //#region src/lib/eas-config.ts
6124
6444
  const MAX_EXTENDS_DEPTH = 10;
@@ -6223,6 +6543,7 @@ const parseBuildProfile = (raw) => {
6223
6543
  const android = parseAndroidProfile(record["android"]);
6224
6544
  const credentialsSource = asCredentialsSource(record["credentialsSource"]);
6225
6545
  const autoIncrement = asAutoIncrement(record["autoIncrement"]);
6546
+ const withoutCredentials = asBooleanValue(record["withoutCredentials"]);
6226
6547
  return {
6227
6548
  ...extendsName === void 0 ? {} : { extends: extendsName },
6228
6549
  ...developmentClient === void 0 ? {} : { developmentClient },
@@ -6233,7 +6554,8 @@ const parseBuildProfile = (raw) => {
6233
6554
  ...ios === void 0 ? {} : { ios },
6234
6555
  ...android === void 0 ? {} : { android },
6235
6556
  ...credentialsSource === void 0 ? {} : { credentialsSource },
6236
- ...autoIncrement === void 0 ? {} : { autoIncrement }
6557
+ ...autoIncrement === void 0 ? {} : { autoIncrement },
6558
+ ...withoutCredentials === void 0 ? {} : { withoutCredentials }
6237
6559
  };
6238
6560
  };
6239
6561
  const parseEasConfig = (text) => Effect.gen(function* () {
@@ -6249,9 +6571,16 @@ const parseEasConfig = (text) => Effect.gen(function* () {
6249
6571
  const profile = parseBuildProfile(value);
6250
6572
  if (profile) profiles[name] = profile;
6251
6573
  }
6574
+ const submitRecord = asRecord(root["submit"]);
6575
+ const submit = {};
6576
+ if (submitRecord) for (const [name, value] of Object.entries(submitRecord)) {
6577
+ const profile = parseSubmitProfile(value);
6578
+ if (profile !== void 0) submit[name] = profile;
6579
+ }
6252
6580
  return {
6253
6581
  ...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
6254
- build: profiles
6582
+ build: profiles,
6583
+ ...Object.keys(submit).length === 0 ? {} : { submit }
6255
6584
  };
6256
6585
  });
6257
6586
  const parseCli = (raw) => {
@@ -6302,6 +6631,7 @@ const mergeProfile = (base, overlay) => {
6302
6631
  const environment = overlay.environment ?? base.environment;
6303
6632
  const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
6304
6633
  const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
6634
+ const withoutCredentials = overlay.withoutCredentials ?? base.withoutCredentials;
6305
6635
  return {
6306
6636
  ...overlay.extends === void 0 ? {} : { extends: overlay.extends },
6307
6637
  ...developmentClient === void 0 ? {} : { developmentClient },
@@ -6312,7 +6642,8 @@ const mergeProfile = (base, overlay) => {
6312
6642
  ...ios === void 0 ? {} : { ios },
6313
6643
  ...android === void 0 ? {} : { android },
6314
6644
  ...credentialsSource === void 0 ? {} : { credentialsSource },
6315
- ...autoIncrement === void 0 ? {} : { autoIncrement }
6645
+ ...autoIncrement === void 0 ? {} : { autoIncrement },
6646
+ ...withoutCredentials === void 0 ? {} : { withoutCredentials }
6316
6647
  };
6317
6648
  };
6318
6649
  const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
@@ -6390,9 +6721,10 @@ const toIosProfile = (eas) => {
6390
6721
  if (!distribution) return;
6391
6722
  const ios = eas.ios ?? {};
6392
6723
  const autoIncrement = resolveIosAutoIncrement(eas);
6724
+ const buildConfiguration = ios.buildConfiguration ?? (eas.developmentClient === true ? "Debug" : void 0);
6393
6725
  return {
6394
6726
  distribution,
6395
- ...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
6727
+ ...buildConfiguration === void 0 ? {} : { buildConfiguration },
6396
6728
  ...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
6397
6729
  ...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
6398
6730
  ...autoIncrement === void 0 ? {} : { autoIncrement }
@@ -6405,10 +6737,11 @@ const toAndroidProfile = (eas) => {
6405
6737
  const android = eas.android ?? {};
6406
6738
  const distribution = deriveAndroidDistribution(eas, format);
6407
6739
  const autoIncrement = resolveAndroidAutoIncrement(eas);
6740
+ const buildType = android.buildType ?? (eas.developmentClient === true ? "debug" : void 0);
6408
6741
  return {
6409
6742
  format,
6410
6743
  distribution,
6411
- ...android.buildType === void 0 ? {} : { buildType: android.buildType },
6744
+ ...buildType === void 0 ? {} : { buildType },
6412
6745
  ...android.flavor === void 0 ? {} : { flavor: android.flavor },
6413
6746
  ...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
6414
6747
  ...autoIncrement === void 0 ? {} : { autoIncrement }
@@ -6424,7 +6757,9 @@ const fromEasProfile = (eas, profileName) => {
6424
6757
  ...eas.env === void 0 ? {} : { env: eas.env },
6425
6758
  ...ios === void 0 ? {} : { ios },
6426
6759
  ...android === void 0 ? {} : { android },
6427
- ...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource }
6760
+ ...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource },
6761
+ ...eas.developmentClient === void 0 ? {} : { developmentClient: eas.developmentClient },
6762
+ ...eas.withoutCredentials === void 0 ? {} : { withoutCredentials: eas.withoutCredentials }
6428
6763
  };
6429
6764
  };
6430
6765
  const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
@@ -6467,6 +6802,51 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
6467
6802
  if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
6468
6803
  });
6469
6804
 
6805
+ //#endregion
6806
+ //#region src/lib/dev-client-check.ts
6807
+ const readDeps = (filePath) => Effect.gen(function* () {
6808
+ const text = yield* (yield* FileSystem.FileSystem).readFileString(filePath).pipe(Effect.option, Effect.catchAllDefect(() => Effect.succeedNone));
6809
+ if (text._tag === "None") return;
6810
+ const parsed = yield* Effect.try({
6811
+ try: () => JSON.parse(text.value),
6812
+ catch: () => void 0
6813
+ }).pipe(Effect.option);
6814
+ if (parsed._tag === "None") return;
6815
+ const root = asRecord(parsed.value);
6816
+ if (!root) return;
6817
+ const dependencies = asRecord(root["dependencies"]);
6818
+ const devDependencies = asRecord(root["devDependencies"]);
6819
+ return {
6820
+ ...dependencies === void 0 ? {} : { dependencies },
6821
+ ...devDependencies === void 0 ? {} : { devDependencies }
6822
+ };
6823
+ });
6824
+ const hasExpoDevClient = (deps) => {
6825
+ if (!deps) return false;
6826
+ return Object.hasOwn(deps.dependencies ?? {}, "expo-dev-client") || Object.hasOwn(deps.devDependencies ?? {}, "expo-dev-client");
6827
+ };
6828
+ /**
6829
+ * Read `<projectRoot>/package.json` and report whether `expo-dev-client` is
6830
+ * declared (either in `dependencies` or `devDependencies`). Returns `false`
6831
+ * when the file is missing or unparseable — callers treat that as "not
6832
+ * installed". Does not walk monorepo workspaces: `expo-dev-client` is an
6833
+ * app-level dep in every Expo template and root-hoisting native deps is
6834
+ * unusual.
6835
+ */
6836
+ const hasDevClientInstalled = (projectRoot) => Effect.gen(function* () {
6837
+ return hasExpoDevClient(yield* readDeps(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => void 0)));
6838
+ });
6839
+ /**
6840
+ * Verify `expo-dev-client` is installed when the profile sets
6841
+ * `developmentClient: true`. Without it the built binary boots straight into
6842
+ * the regular app (no launcher, no Metro switcher) and the user cannot connect
6843
+ * a dev server. EAS warns and proceeds; we mirror that — warn, do not block.
6844
+ */
6845
+ const warnIfDevClientMissing = (projectRoot) => Effect.gen(function* () {
6846
+ if (yield* hasDevClientInstalled(projectRoot)) return;
6847
+ yield* printWarn("expo-dev-client is not in dependencies. The built artifact will boot the regular app without a Metro launcher. Install it with `bunx expo install expo-dev-client` and rebuild.");
6848
+ });
6849
+
6470
6850
  //#endregion
6471
6851
  //#region src/lib/env-exporter.ts
6472
6852
  const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
@@ -6587,7 +6967,7 @@ const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? inpu
6587
6967
 
6588
6968
  //#endregion
6589
6969
  //#region src/lib/platform-detect.ts
6590
- const PLATFORMS = ["ios", "android"];
6970
+ const PLATFORMS$1 = ["ios", "android"];
6591
6971
  const inferPlatforms = (config) => {
6592
6972
  const fromConfig = config["platforms"];
6593
6973
  if (Array.isArray(fromConfig)) return fromConfig.filter((entry) => entry === "ios" || entry === "android");
@@ -6612,7 +6992,7 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
6612
6992
  return only;
6613
6993
  }
6614
6994
  if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms detected (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
6615
- return yield* promptSelect("Which platform to build?", PLATFORMS.filter((entry) => candidates.includes(entry)).map((entry) => ({
6995
+ return yield* promptSelect("Which platform to build?", PLATFORMS$1.filter((entry) => candidates.includes(entry)).map((entry) => ({
6616
6996
  value: entry,
6617
6997
  label: entry
6618
6998
  })));
@@ -6620,7 +7000,7 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
6620
7000
 
6621
7001
  //#endregion
6622
7002
  //#region src/lib/project-staging.ts
6623
- const execFileAsync = promisify(execFile);
7003
+ const execFileAsync$1 = promisify(execFile);
6624
7004
  const LOCKFILES = [
6625
7005
  ["bun.lock", "bun"],
6626
7006
  ["bun.lockb", "bun"],
@@ -6716,7 +7096,7 @@ const copyProjectTree = (params) => Effect.tryPromise({
6716
7096
  * they exist only so `git rev-parse` succeeds during postinstall.
6717
7097
  */
6718
7098
  const initGitRepo = (stagingRoot) => Effect.tryPromise({
6719
- try: async () => execFileAsync("git", [
7099
+ try: async () => execFileAsync$1("git", [
6720
7100
  "init",
6721
7101
  "-q",
6722
7102
  stagingRoot
@@ -6799,8 +7179,566 @@ const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(f
6799
7179
  return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
6800
7180
  });
6801
7181
 
7182
+ //#endregion
7183
+ //#region src/lib/google-play.ts
7184
+ var GooglePlayAuthError = class extends Schema.TaggedError()("GooglePlayAuthError", {
7185
+ message: Schema.String,
7186
+ cause: Schema.optional(Schema.Unknown)
7187
+ }) {};
7188
+ var GooglePlayApiError = class extends Schema.TaggedError()("GooglePlayApiError", {
7189
+ message: Schema.String,
7190
+ httpStatus: Schema.optional(Schema.Number),
7191
+ cause: Schema.optional(Schema.Unknown)
7192
+ }) {};
7193
+ const ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
7194
+ const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
7195
+ const ANDROID_PUBLISHER_BASE = "https://androidpublisher.googleapis.com";
7196
+ const ANDROID_UPLOAD_BASE = "https://androidpublisher.googleapis.com/upload";
7197
+ const ServiceAccountJsonSchema = Schema.Struct({
7198
+ type: Schema.String,
7199
+ client_email: Schema.String,
7200
+ private_key: Schema.String,
7201
+ token_uri: Schema.optional(Schema.String)
7202
+ });
7203
+ const TokenResponseSchema = Schema.Struct({
7204
+ access_token: Schema.String,
7205
+ expires_in: Schema.optional(Schema.Number)
7206
+ });
7207
+ const stripPemHeaders = (pem) => pem.replace(/-----BEGIN [A-Z ]+-----/u, "").replace(/-----END [A-Z ]+-----/u, "").replaceAll(/\s+/gu, "");
7208
+ const asArrayBuffer = (bytes) => {
7209
+ const buffer = new ArrayBuffer(bytes.byteLength);
7210
+ new Uint8Array(buffer).set(bytes);
7211
+ return buffer;
7212
+ };
7213
+ const importPrivateKey = (pem) => Effect.tryPromise({
7214
+ try: async () => {
7215
+ const pkcs8 = fromBase64(stripPemHeaders(pem));
7216
+ return crypto.subtle.importKey("pkcs8", asArrayBuffer(pkcs8), {
7217
+ name: "RSASSA-PKCS1-v1_5",
7218
+ hash: "SHA-256"
7219
+ }, false, ["sign"]);
7220
+ },
7221
+ catch: (cause) => new GooglePlayAuthError({
7222
+ message: "Failed to import service account private key",
7223
+ cause
7224
+ })
7225
+ });
7226
+ const buildJwtAssertion = (params) => {
7227
+ const header = {
7228
+ alg: "RS256",
7229
+ typ: "JWT"
7230
+ };
7231
+ const claims = {
7232
+ iss: params.clientEmail,
7233
+ scope: ANDROID_PUBLISHER_SCOPE,
7234
+ aud: params.tokenUri,
7235
+ exp: params.nowSeconds + 3600,
7236
+ iat: params.nowSeconds
7237
+ };
7238
+ return `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(claims)))}`;
7239
+ };
7240
+ const signJwt = (key, payload) => Effect.tryPromise({
7241
+ try: async () => {
7242
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(payload));
7243
+ return `${payload}.${toBase64Url(new Uint8Array(signature))}`;
7244
+ },
7245
+ catch: (cause) => new GooglePlayAuthError({
7246
+ message: "Failed to sign JWT",
7247
+ cause
7248
+ })
7249
+ });
7250
+ const postTokenRequest = (tokenUri, jwt) => Effect.tryPromise({
7251
+ try: async () => {
7252
+ const body = new URLSearchParams({
7253
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
7254
+ assertion: jwt
7255
+ });
7256
+ const response = await fetch(tokenUri, {
7257
+ method: "POST",
7258
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
7259
+ body
7260
+ });
7261
+ const text = await response.text();
7262
+ return {
7263
+ ok: response.ok,
7264
+ status: response.status,
7265
+ text
7266
+ };
7267
+ },
7268
+ catch: (cause) => new GooglePlayAuthError({
7269
+ message: "Failed to exchange JWT for access token",
7270
+ cause
7271
+ })
7272
+ });
7273
+ const exchangeJwtForAccessToken = (tokenUri, jwt) => Effect.gen(function* () {
7274
+ const result = yield* postTokenRequest(tokenUri, jwt);
7275
+ if (!result.ok) return yield* Effect.fail(new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` }));
7276
+ const json = yield* Effect.try({
7277
+ try: () => JSON.parse(result.text),
7278
+ catch: (cause) => new GooglePlayAuthError({
7279
+ message: "OAuth token response is not JSON",
7280
+ cause
7281
+ })
7282
+ });
7283
+ return (yield* Schema.decodeUnknown(TokenResponseSchema)(json).pipe(Effect.mapError((cause) => new GooglePlayAuthError({
7284
+ message: "OAuth token response missing access_token",
7285
+ cause
7286
+ })))).access_token;
7287
+ });
7288
+ const acquireGooglePlayAccessToken = (serviceAccountJson) => Effect.gen(function* () {
7289
+ const raw = yield* Effect.try({
7290
+ try: () => JSON.parse(serviceAccountJson),
7291
+ catch: (cause) => new GooglePlayAuthError({
7292
+ message: "Service account JSON is not valid JSON",
7293
+ cause
7294
+ })
7295
+ });
7296
+ const parsed = yield* Schema.decodeUnknown(ServiceAccountJsonSchema)(raw).pipe(Effect.mapError((cause) => new GooglePlayAuthError({
7297
+ message: "Service account JSON missing required fields (type, client_email, private_key)",
7298
+ cause
7299
+ })));
7300
+ if (parsed.type !== "service_account") return yield* Effect.fail(new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` }));
7301
+ const tokenUri = parsed.token_uri ?? GOOGLE_OAUTH_TOKEN_URL;
7302
+ return {
7303
+ accessToken: yield* exchangeJwtForAccessToken(tokenUri, yield* signJwt(yield* importPrivateKey(parsed.private_key), buildJwtAssertion({
7304
+ clientEmail: parsed.client_email,
7305
+ tokenUri,
7306
+ nowSeconds: Math.floor(Date.now() / 1e3)
7307
+ }))),
7308
+ clientEmail: parsed.client_email
7309
+ };
7310
+ });
7311
+ const authHeaders = (accessToken) => ({ Authorization: `Bearer ${accessToken}` });
7312
+ const performFetch = (params) => Effect.tryPromise({
7313
+ try: async () => {
7314
+ const init = params.body === void 0 ? {
7315
+ method: params.method,
7316
+ headers: authHeaders(params.accessToken)
7317
+ } : {
7318
+ method: params.method,
7319
+ headers: {
7320
+ ...authHeaders(params.accessToken),
7321
+ "Content-Type": "application/json"
7322
+ },
7323
+ body: JSON.stringify(params.body)
7324
+ };
7325
+ const response = await fetch(params.url, init);
7326
+ const text = await response.text();
7327
+ return {
7328
+ ok: response.ok,
7329
+ status: response.status,
7330
+ text
7331
+ };
7332
+ },
7333
+ catch: (cause) => new GooglePlayApiError({
7334
+ message: `${params.label} request failed`,
7335
+ cause
7336
+ })
7337
+ });
7338
+ const callJsonRaw = (params) => Effect.gen(function* () {
7339
+ const result = yield* performFetch(params);
7340
+ if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
7341
+ message: `${params.label} failed: ${String(result.status)} ${result.text}`,
7342
+ httpStatus: result.status
7343
+ }));
7344
+ return yield* Effect.try({
7345
+ try: () => result.text === "" ? {} : JSON.parse(result.text),
7346
+ catch: (cause) => new GooglePlayApiError({
7347
+ message: `${params.label} response is not JSON`,
7348
+ cause
7349
+ })
7350
+ });
7351
+ });
7352
+ const AppEditSchema = Schema.Struct({
7353
+ id: Schema.String,
7354
+ expiryTimeSeconds: Schema.optional(Schema.String)
7355
+ });
7356
+ const insertEdit = (params) => Effect.gen(function* () {
7357
+ const raw = yield* callJsonRaw({
7358
+ url: `${ANDROID_PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(params.packageName)}/edits`,
7359
+ method: "POST",
7360
+ accessToken: params.accessToken,
7361
+ body: {},
7362
+ label: "edits.insert"
7363
+ });
7364
+ return yield* Schema.decodeUnknown(AppEditSchema)(raw).pipe(Effect.mapError((cause) => new GooglePlayApiError({
7365
+ message: "edits.insert response missing id",
7366
+ cause
7367
+ })));
7368
+ });
7369
+ const UploadedBundleSchema = Schema.Struct({
7370
+ versionCode: Schema.Number,
7371
+ sha256: Schema.optional(Schema.String)
7372
+ });
7373
+ const performBundleUpload = (params) => Effect.tryPromise({
7374
+ try: async () => {
7375
+ const url = `${ANDROID_UPLOAD_BASE}/androidpublisher/v3/applications/${encodeURIComponent(params.packageName)}/edits/${encodeURIComponent(params.editId)}/bundles?uploadType=media`;
7376
+ const response = await fetch(url, {
7377
+ method: "POST",
7378
+ headers: {
7379
+ ...authHeaders(params.accessToken),
7380
+ "Content-Type": "application/octet-stream"
7381
+ },
7382
+ body: new Uint8Array(params.aabBytes)
7383
+ });
7384
+ const text = await response.text();
7385
+ return {
7386
+ ok: response.ok,
7387
+ status: response.status,
7388
+ text
7389
+ };
7390
+ },
7391
+ catch: (cause) => new GooglePlayApiError({
7392
+ message: "edits.bundles.upload request failed",
7393
+ cause
7394
+ })
7395
+ });
7396
+ const uploadBundle = (params) => Effect.gen(function* () {
7397
+ const result = yield* performBundleUpload(params);
7398
+ if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
7399
+ message: `edits.bundles.upload failed: ${String(result.status)} ${result.text}`,
7400
+ httpStatus: result.status
7401
+ }));
7402
+ const raw = yield* Effect.try({
7403
+ try: () => JSON.parse(result.text),
7404
+ catch: (cause) => new GooglePlayApiError({
7405
+ message: "edits.bundles.upload response is not JSON",
7406
+ cause
7407
+ })
7408
+ });
7409
+ return yield* Schema.decodeUnknown(UploadedBundleSchema)(raw).pipe(Effect.mapError((cause) => new GooglePlayApiError({
7410
+ message: "Bundle upload response missing versionCode",
7411
+ cause
7412
+ })));
7413
+ });
7414
+ const updateTrack = (params) => {
7415
+ const release = {
7416
+ status: params.releaseStatus,
7417
+ versionCodes: [String(params.versionCode)],
7418
+ ...params.rollout === null ? {} : { userFraction: params.rollout },
7419
+ ...params.releaseNotes === void 0 ? {} : { releaseNotes: [{
7420
+ language: "en-US",
7421
+ text: params.releaseNotes
7422
+ }] }
7423
+ };
7424
+ return callJsonRaw({
7425
+ url: `${ANDROID_PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(params.packageName)}/edits/${encodeURIComponent(params.editId)}/tracks/${encodeURIComponent(params.track)}`,
7426
+ method: "PUT",
7427
+ accessToken: params.accessToken,
7428
+ body: {
7429
+ track: params.track,
7430
+ releases: [release]
7431
+ },
7432
+ label: "edits.tracks.update"
7433
+ });
7434
+ };
7435
+ const commitEdit = (params) => callJsonRaw({
7436
+ url: `${ANDROID_PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(params.packageName)}/edits/${encodeURIComponent(params.editId)}:commit?changesNotSentForReview=${String(params.changesNotSentForReview)}`,
7437
+ method: "POST",
7438
+ accessToken: params.accessToken,
7439
+ label: "edits.commit"
7440
+ });
7441
+
7442
+ //#endregion
7443
+ //#region src/lib/nullable.ts
7444
+ const toDbNull = (value) => value ?? null;
7445
+
7446
+ //#endregion
7447
+ //#region src/application/submit-flow.ts
7448
+ const execFileAsync = promisify(execFile);
7449
+ const ExecErrorSchema = Schema.Struct({
7450
+ code: Schema.optional(Schema.Number),
7451
+ stdout: Schema.optional(Schema.String),
7452
+ stderr: Schema.optional(Schema.String)
7453
+ });
7454
+ const runAltool = (args) => Effect.tryPromise({
7455
+ try: async () => {
7456
+ const { stdout, stderr } = await execFileAsync("xcrun", ["altool", ...args]);
7457
+ return {
7458
+ exitCode: 0,
7459
+ stdout,
7460
+ stderr
7461
+ };
7462
+ },
7463
+ catch: (error) => {
7464
+ const parsed = Schema.decodeUnknownSync(ExecErrorSchema, { onExcessProperty: "ignore" })(typeof error === "object" && error !== null ? error : {});
7465
+ const stdout = parsed.stdout ?? "";
7466
+ const stderr = parsed.stderr ?? String(error);
7467
+ return {
7468
+ exitCode: parsed.code ?? 1,
7469
+ stdout,
7470
+ stderr: stderr === "" ? String(error) : stderr
7471
+ };
7472
+ }
7473
+ }).pipe(Effect.catchAll((result) => Effect.succeed(result)));
7474
+ var CliSubmitError = class extends Schema.TaggedError()("CliSubmitError", {
7475
+ code: Schema.String,
7476
+ message: Schema.String
7477
+ }) {};
7478
+ const createSubmissionViaApi = (api, resolved) => api.submissions.create({
7479
+ path: { projectId: resolved.projectId },
7480
+ payload: {
7481
+ platform: resolved.platform,
7482
+ profileName: resolved.profileName,
7483
+ archiveSource: resolved.archiveSource,
7484
+ ...resolved.buildId === void 0 ? {} : { buildId: resolved.buildId },
7485
+ ...resolved.archiveUrl === void 0 ? {} : { archiveUrl: resolved.archiveUrl },
7486
+ ...resolved.iosConfig === void 0 ? {} : { iosConfig: resolved.iosConfig },
7487
+ ...resolved.androidConfig === void 0 ? {} : { androidConfig: resolved.androidConfig }
7488
+ }
7489
+ }).pipe(Effect.mapError(() => new CliSubmitError({
7490
+ code: "SUBMISSION_CREATE_FAILED",
7491
+ message: "Failed to create submission via API"
7492
+ })));
7493
+ const isTerminal = (status) => status === "FINISHED" || status === "ERRORED" || status === "CANCELED";
7494
+ const fetchSubmission = (api, submissionId) => api.submissions.get({ path: { id: submissionId } }).pipe(Effect.mapError(() => new CliSubmitError({
7495
+ code: "SUBMISSION_GET_FAILED",
7496
+ message: "Failed to read submission status"
7497
+ })));
7498
+ const pollSubmissionUntilTerminal = (api, submissionId, pollIntervalMs = 5e3) => Effect.iterate(void 0, {
7499
+ while: (state) => state === void 0 || !isTerminal(state.status),
7500
+ body: (state) => Effect.gen(function* () {
7501
+ if (state !== void 0) yield* Effect.sleep(Duration.millis(pollIntervalMs));
7502
+ const next = yield* fetchSubmission(api, submissionId);
7503
+ yield* printHuman(`status: ${next.status}`);
7504
+ return next;
7505
+ })
7506
+ }).pipe(Effect.flatMap((final) => final === void 0 ? Effect.fail(new CliSubmitError({
7507
+ code: "SUBMISSION_POLL_NO_RESULT",
7508
+ message: "Polling completed without producing a submission"
7509
+ })) : Effect.succeed(final)));
7510
+ const writeAscApiKeyP8 = (api, ascApiKeyId) => Effect.gen(function* () {
7511
+ const creds = yield* fetchAscCredentials(api, ascApiKeyId).pipe(Effect.mapError(() => new CliSubmitError({
7512
+ code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
7513
+ message: `Failed to download ASC API key ${ascApiKeyId}`
7514
+ })));
7515
+ const target = path.join(tmpdir(), `better-update-submit-AuthKey_${creds.keyId}.p8`);
7516
+ yield* Effect.promise(async () => writeFile(target, fromBase64(creds.p8Pem)));
7517
+ return {
7518
+ p8Path: target,
7519
+ keyId: creds.keyId,
7520
+ issuerId: creds.issuerId
7521
+ };
7522
+ });
7523
+ const runIosAltoolUpload = (inputs) => Effect.gen(function* () {
7524
+ const creds = yield* writeAscApiKeyP8(inputs.api, inputs.ascApiKeyId);
7525
+ const apiKeyDir = path.dirname(creds.p8Path);
7526
+ yield* inputs.api.submissions.updateStatus({
7527
+ path: { id: inputs.submissionId },
7528
+ payload: { status: "IN_PROGRESS" }
7529
+ }).pipe(Effect.mapError(() => new CliSubmitError({
7530
+ code: "SUBMISSION_PATCH_FAILED",
7531
+ message: "Failed to PATCH submission status to IN_PROGRESS"
7532
+ })));
7533
+ const result = yield* runAltool([
7534
+ "--upload-app",
7535
+ "--type",
7536
+ "ios",
7537
+ "--apiKey",
7538
+ creds.keyId,
7539
+ "--apiIssuer",
7540
+ creds.issuerId,
7541
+ "--apiKeyDir",
7542
+ apiKeyDir,
7543
+ "--file",
7544
+ inputs.ipaPath,
7545
+ "--output-format",
7546
+ "xml"
7547
+ ]);
7548
+ const terminalStatus = result.exitCode === 0 ? "FINISHED" : "ERRORED";
7549
+ const errorMessage = result.exitCode === 0 ? null : `xcrun altool exited ${String(result.exitCode)}: ${result.stderr}`;
7550
+ yield* inputs.api.submissions.updateStatus({
7551
+ path: { id: inputs.submissionId },
7552
+ payload: {
7553
+ status: terminalStatus,
7554
+ ...errorMessage === null ? {} : {
7555
+ errorCode: "SUBMISSION_SERVICE_IOS_ALTOOL_FAILED",
7556
+ errorMessage
7557
+ }
7558
+ }
7559
+ }).pipe(Effect.mapError(() => new CliSubmitError({
7560
+ code: "SUBMISSION_PATCH_FAILED",
7561
+ message: "Failed to PATCH submission terminal status"
7562
+ })));
7563
+ return {
7564
+ status: terminalStatus,
7565
+ stdout: result.stdout,
7566
+ stderr: result.stderr
7567
+ };
7568
+ });
7569
+ const readLocalFile = (filePath, errorCode, errorMessageFmt) => Effect.tryPromise({
7570
+ try: async () => readFile(filePath),
7571
+ catch: (cause) => new CliSubmitError({
7572
+ code: errorCode,
7573
+ message: errorMessageFmt(cause)
7574
+ })
7575
+ });
7576
+ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
7577
+ const result = yield* Effect.tryPromise({
7578
+ try: async () => {
7579
+ const response = await fetch(url);
7580
+ const bytes = response.ok ? new Uint8Array(await response.arrayBuffer()) : null;
7581
+ return {
7582
+ ok: response.ok,
7583
+ status: response.status,
7584
+ bytes
7585
+ };
7586
+ },
7587
+ catch: (cause) => new CliSubmitError({
7588
+ code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
7589
+ message: `Failed to download AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
7590
+ })
7591
+ });
7592
+ if (!result.ok || result.bytes === null) return yield* Effect.fail(new CliSubmitError({
7593
+ code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
7594
+ message: `HTTP ${String(result.status)} fetching archive at ${url}`
7595
+ }));
7596
+ return result.bytes;
7597
+ });
7598
+ 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);
7599
+ const fetchServiceAccountKeyById = (api, id) => api.googleServiceAccountKeys.download({ path: { id } }).pipe(Effect.mapError(() => new CliSubmitError({
7600
+ code: "SUBMISSION_ANDROID_SA_KEY_FETCH_FAILED",
7601
+ message: `Failed to download Google service account key ${id}`
7602
+ })), Effect.map((data) => data.json));
7603
+ const resolveServiceAccountJson = (params) => {
7604
+ if (params.serviceAccountKeyId !== void 0) return fetchServiceAccountKeyById(params.api, params.serviceAccountKeyId);
7605
+ 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));
7606
+ return Effect.fail(new CliSubmitError({
7607
+ code: "SUBMISSION_ANDROID_SA_KEY_MISSING",
7608
+ 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."
7609
+ }));
7610
+ };
7611
+ const patchSubmissionStatus = (api, submissionId, payload) => api.submissions.updateStatus({
7612
+ path: { id: submissionId },
7613
+ payload
7614
+ }).pipe(Effect.mapError(() => new CliSubmitError({
7615
+ code: "SUBMISSION_PATCH_FAILED",
7616
+ message: `Failed to PATCH submission status to ${payload.status}`
7617
+ })));
7618
+ const wrapGooglePlayError = (label) => (cause) => new CliSubmitError({
7619
+ code: `SUBMISSION_ANDROID_${label}`,
7620
+ message: cause.message
7621
+ });
7622
+ const runGooglePlayPipeline = (params) => Effect.gen(function* () {
7623
+ const edit = yield* insertEdit({
7624
+ accessToken: params.accessToken,
7625
+ packageName: params.applicationId
7626
+ }).pipe(Effect.mapError(wrapGooglePlayError("EDIT_INSERT_FAILED")));
7627
+ const uploaded = yield* uploadBundle({
7628
+ accessToken: params.accessToken,
7629
+ packageName: params.applicationId,
7630
+ editId: edit.id,
7631
+ aabBytes: params.aab
7632
+ }).pipe(Effect.mapError(wrapGooglePlayError("BUNDLE_UPLOAD_FAILED")));
7633
+ yield* updateTrack({
7634
+ accessToken: params.accessToken,
7635
+ packageName: params.applicationId,
7636
+ editId: edit.id,
7637
+ track: params.track,
7638
+ releaseStatus: params.releaseStatus,
7639
+ versionCode: uploaded.versionCode,
7640
+ rollout: params.rollout
7641
+ }).pipe(Effect.mapError(wrapGooglePlayError("TRACK_UPDATE_FAILED")));
7642
+ yield* commitEdit({
7643
+ accessToken: params.accessToken,
7644
+ packageName: params.applicationId,
7645
+ editId: edit.id,
7646
+ changesNotSentForReview: params.changesNotSentForReview
7647
+ }).pipe(Effect.mapError(wrapGooglePlayError("COMMIT_FAILED")));
7648
+ return uploaded;
7649
+ });
7650
+ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
7651
+ const { applicationId } = inputs.androidProfile;
7652
+ if (applicationId === void 0) return yield* Effect.fail(new CliSubmitError({
7653
+ code: "SUBMISSION_ANDROID_APP_ID_MISSING",
7654
+ message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
7655
+ }));
7656
+ const serviceAccountJson = yield* resolveServiceAccountJson({
7657
+ api: inputs.api,
7658
+ serviceAccountKeyId: inputs.serviceAccountKeyId,
7659
+ serviceAccountKeyPath: inputs.androidProfile.serviceAccountKeyPath
7660
+ });
7661
+ yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "IN_PROGRESS" });
7662
+ const result = yield* Effect.gen(function* () {
7663
+ const token = yield* acquireGooglePlayAccessToken(serviceAccountJson).pipe(Effect.mapError(wrapGooglePlayError("AUTH_FAILED")));
7664
+ const aab = yield* readArchiveBytes(inputs.archive);
7665
+ return yield* runGooglePlayPipeline({
7666
+ accessToken: token.accessToken,
7667
+ applicationId,
7668
+ aab,
7669
+ track: inputs.androidProfile.track ?? "internal",
7670
+ releaseStatus: inputs.androidProfile.releaseStatus ?? "completed",
7671
+ changesNotSentForReview: inputs.androidProfile.changesNotSentForReview ?? false,
7672
+ rollout: toDbNull(inputs.androidProfile.rollout)
7673
+ });
7674
+ }).pipe(Effect.catchTag("CliSubmitError", (engineError) => Effect.gen(function* () {
7675
+ yield* patchSubmissionStatus(inputs.api, inputs.submissionId, {
7676
+ status: "ERRORED",
7677
+ errorCode: engineError.code,
7678
+ errorMessage: engineError.message
7679
+ });
7680
+ return yield* Effect.fail(engineError);
7681
+ })));
7682
+ yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
7683
+ yield* printHuman(`Google Play bundle uploaded (versionCode ${String(result.versionCode)})`);
7684
+ return result;
7685
+ });
7686
+
6802
7687
  //#endregion
6803
7688
  //#region src/application/build-workflow.ts
7689
+ const buildAutoSubmitIosConfig = (iosProfile, whatToTest) => {
7690
+ if (iosProfile?.bundleIdentifier === void 0) return;
7691
+ return {
7692
+ bundleIdentifier: iosProfile.bundleIdentifier,
7693
+ ...iosProfile.appleId === void 0 ? {} : { appleId: iosProfile.appleId },
7694
+ ...iosProfile.ascAppId === void 0 ? {} : { ascAppId: iosProfile.ascAppId },
7695
+ ...iosProfile.appleTeamId === void 0 ? {} : { appleTeamId: iosProfile.appleTeamId },
7696
+ ...iosProfile.sku === void 0 ? {} : { sku: iosProfile.sku },
7697
+ ...iosProfile.language === void 0 ? {} : { language: iosProfile.language },
7698
+ ...iosProfile.companyName === void 0 ? {} : { companyName: iosProfile.companyName },
7699
+ ...iosProfile.appName === void 0 ? {} : { appName: iosProfile.appName },
7700
+ ...iosProfile.groups === void 0 ? {} : { groups: iosProfile.groups },
7701
+ ...whatToTest === void 0 ? {} : { whatToTest }
7702
+ };
7703
+ };
7704
+ const buildAutoSubmitAndroidConfig = (androidProfile) => {
7705
+ if (androidProfile?.applicationId === void 0) return;
7706
+ return {
7707
+ applicationId: androidProfile.applicationId,
7708
+ ...androidProfile.track === void 0 ? {} : { track: androidProfile.track },
7709
+ ...androidProfile.releaseStatus === void 0 ? {} : { releaseStatus: androidProfile.releaseStatus },
7710
+ ...androidProfile.changesNotSentForReview === void 0 ? {} : { changesNotSentForReview: androidProfile.changesNotSentForReview },
7711
+ ...androidProfile.rollout === void 0 ? {} : { rollout: androidProfile.rollout }
7712
+ };
7713
+ };
7714
+ const runAutoSubmit = (input) => Effect.gen(function* () {
7715
+ yield* printHuman(`\nAuto-submitting build ${input.buildId} (profile ${input.profileName})...`);
7716
+ const easProfile = yield* resolveEasSubmitProfile((yield* readEasJson(process.cwd())).submit, input.profileName);
7717
+ const archiveUrl = (yield* input.api.builds.getInstallLink({ path: { id: input.buildId } })).artifactUrl;
7718
+ const iosConfig = input.platform === "ios" ? buildAutoSubmitIosConfig(easProfile.ios, input.whatToTest) : void 0;
7719
+ const androidConfig = input.platform === "android" ? buildAutoSubmitAndroidConfig(easProfile.android) : void 0;
7720
+ const submission = yield* createSubmissionViaApi(input.api, {
7721
+ projectId: input.projectId,
7722
+ platform: input.platform,
7723
+ profileName: input.profileName,
7724
+ archiveSource: "build",
7725
+ buildId: input.buildId,
7726
+ archiveUrl,
7727
+ ...iosConfig === void 0 ? {} : { iosConfig },
7728
+ ...androidConfig === void 0 ? {} : { androidConfig }
7729
+ });
7730
+ yield* printHuman(`Submission created: ${submission.id} (${submission.status})`);
7731
+ if (input.platform === "ios" && easProfile.ios?.ascApiKeyId !== void 0) {
7732
+ yield* printHuman("Running xcrun altool upload...");
7733
+ yield* runIosAltoolUpload({
7734
+ api: input.api,
7735
+ submissionId: submission.id,
7736
+ ipaPath: archiveUrl,
7737
+ ascApiKeyId: easProfile.ios.ascApiKeyId
7738
+ });
7739
+ }
7740
+ yield* printHuman(`Submission final status: ${(yield* pollSubmissionUntilTerminal(input.api, submission.id)).status}`);
7741
+ });
6804
7742
  const runIosPlatformBuild = (input) => Effect.gen(function* () {
6805
7743
  const { api, appMeta, envVars, options, profile, projectId, projectRoot, tempDir } = input;
6806
7744
  if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
@@ -6849,7 +7787,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
6849
7787
  yield* warnOnGradleMismatch(gradleConfig, androidBundleId);
6850
7788
  const applicationIdentifier = gradleConfig?.applicationId ?? androidBundleId;
6851
7789
  const credentialsSource = profile.credentialsSource ?? "remote";
6852
- if (credentialsSource === "remote") yield* ensureAndroidCredentials(api, {
7790
+ const skipCredentials = profile.developmentClient === true || profile.withoutCredentials === true;
7791
+ if (credentialsSource === "remote" && !skipCredentials) yield* ensureAndroidCredentials(api, {
6853
7792
  projectId,
6854
7793
  applicationIdentifier
6855
7794
  }, { freezeCredentials: options.freezeCredentials ?? false });
@@ -6863,7 +7802,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
6863
7802
  envVars,
6864
7803
  projectId,
6865
7804
  credentialsSource,
6866
- profileName: profile.name
7805
+ profileName: profile.name,
7806
+ skipCredentials
6867
7807
  }),
6868
7808
  target: androidProfile.format === "aab" ? {
6869
7809
  platform: "android",
@@ -6901,10 +7841,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6901
7841
  const projectId = yield* extractProjectId(baseConfig);
6902
7842
  const platform = yield* detectPlatform(options.platform, baseConfig);
6903
7843
  const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
6904
- const envVars = yield* pullEnvVars(api, {
6905
- projectId,
6906
- environment: profile.environment
6907
- });
7844
+ if (profile.developmentClient === true) yield* warnIfDevClientMissing(userCwd);
7845
+ const envVars = {
7846
+ ...yield* pullEnvVars(api, {
7847
+ projectId,
7848
+ environment: profile.environment
7849
+ }),
7850
+ ...profile.env
7851
+ };
6908
7852
  yield* applyAutoIncrement({
6909
7853
  projectRoot: userCwd,
6910
7854
  platform,
@@ -6991,6 +7935,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6991
7935
  ["SHA-256", build.sha256],
6992
7936
  ["Bytes", String(build.byteSize)]
6993
7937
  ]);
7938
+ if (options.autoSubmit === true) yield* runAutoSubmit({
7939
+ api,
7940
+ buildId: result.id,
7941
+ projectId,
7942
+ platform,
7943
+ profileName: options.autoSubmitProfile ?? profile.name,
7944
+ ...options.whatToTest === void 0 ? {} : { whatToTest: options.whatToTest }
7945
+ });
6994
7946
  }));
6995
7947
 
6996
7948
  //#endregion
@@ -7153,6 +8105,19 @@ const buildCommand = defineCommand({
7153
8105
  "allow-dirty": {
7154
8106
  type: "boolean",
7155
8107
  description: "Proceed even with uncommitted git changes"
8108
+ },
8109
+ "auto-submit": {
8110
+ type: "boolean",
8111
+ alias: "s",
8112
+ description: "After upload, submit the build using eas.json submit profile of the same name"
8113
+ },
8114
+ "auto-submit-with-profile": {
8115
+ type: "string",
8116
+ description: "After upload, submit the build using a specific submit profile"
8117
+ },
8118
+ "what-to-test": {
8119
+ type: "string",
8120
+ description: "iOS-only TestFlight changelog when auto-submitting"
7156
8121
  }
7157
8122
  },
7158
8123
  run: async ({ args }) => runEffect(runBuildWorkflow({
@@ -7164,7 +8129,13 @@ const buildCommand = defineCommand({
7164
8129
  rawOutput: args["raw-output"] ?? false,
7165
8130
  clearCache: args["clear-cache"] ?? false,
7166
8131
  freezeCredentials: args["freeze-credentials"] ?? false,
7167
- allowDirty: args["allow-dirty"] ?? false
8132
+ allowDirty: args["allow-dirty"] ?? false,
8133
+ ...args["auto-submit"] === void 0 ? {} : { autoSubmit: args["auto-submit"] },
8134
+ ...args["auto-submit-with-profile"] === void 0 ? {} : {
8135
+ autoSubmit: true,
8136
+ autoSubmitProfile: args["auto-submit-with-profile"]
8137
+ },
8138
+ ...args["what-to-test"] === void 0 ? {} : { whatToTest: args["what-to-test"] }
7168
8139
  }), BUILD_EXIT_EXTRAS)
7169
8140
  });
7170
8141
 
@@ -7431,7 +8402,18 @@ const listCommand$5 = defineCommand({
7431
8402
 
7432
8403
  //#endregion
7433
8404
  //#region src/commands/builds/resign.ts
7434
- const resignWorkflowText = (buildId, installLink) => `Resigning iOS build ${buildId}
8405
+ const resignWorkflowText = (params) => {
8406
+ const inputs = params.resolved;
8407
+ const profilePathHint = inputs === void 0 ? "/path/to/new.mobileprovision" : inputs.profilePath;
8408
+ const identityHint = inputs === void 0 ? `"iPhone Distribution: Your Team (ABCDE12345)"` : `"${inputs.signingIdentity}"`;
8409
+ const appName = inputs === void 0 ? "YourApp.app" : `${inputs.appName}.app`;
8410
+ const resolvedHeader = inputs === void 0 ? "" : `Resolved inputs
8411
+ Profile: ${inputs.profilePath}
8412
+ Identity: ${inputs.signingIdentity}
8413
+ Target app bundle: ${inputs.appName}.app
8414
+
8415
+ `;
8416
+ return `Resigning iOS build ${params.buildId}
7435
8417
  =================================================
7436
8418
 
7437
8419
  iOS code-signing requires native macOS tooling (codesign, security, xcodebuild)
@@ -7439,22 +8421,22 @@ and the matching distribution certificate in your Keychain. better-update does
7439
8421
  not bundle that toolchain — instead it gives you the inputs and a re-upload
7440
8422
  path.
7441
8423
 
7442
- Step 1 — Download the existing IPA
7443
- ${installLink}
8424
+ ${resolvedHeader}Step 1 — Download the existing IPA
8425
+ ${params.installLink}
7444
8426
 
7445
8427
  Step 2 — Resign the IPA locally with your new provisioning profile.
7446
8428
  Pick one of:
7447
8429
  a) fastlane sigh resign:
7448
8430
  fastlane sigh resign /tmp/build.ipa \\
7449
- --signing_identity "iPhone Distribution: Your Team (ABCDE12345)" \\
7450
- --provisioning_profile /path/to/new.mobileprovision
8431
+ --signing_identity ${identityHint} \\
8432
+ --provisioning_profile ${profilePathHint}
7451
8433
 
7452
8434
  b) Apple's codesign + xcodebuild:
7453
8435
  unzip /tmp/build.ipa -d /tmp/payload
7454
- cp /path/to/new.mobileprovision /tmp/payload/Payload/YourApp.app/embedded.mobileprovision
7455
- codesign -f -s "iPhone Distribution: Your Team (ABCDE12345)" \\
7456
- --entitlements <(security cms -D -i /path/to/new.mobileprovision) \\
7457
- /tmp/payload/Payload/YourApp.app
8436
+ cp ${profilePathHint} /tmp/payload/Payload/${appName}/embedded.mobileprovision
8437
+ codesign -f -s ${identityHint} \\
8438
+ --entitlements <(security cms -D -i ${profilePathHint}) \\
8439
+ /tmp/payload/Payload/${appName}
7458
8440
  (cd /tmp/payload && zip -qr /tmp/resigned.ipa Payload)
7459
8441
 
7460
8442
  Step 3 — Upload the re-signed IPA as a fresh build:
@@ -7464,16 +8446,40 @@ Step 3 — Upload the re-signed IPA as a fresh build:
7464
8446
  The new build will get a fresh build ID. The original build remains for
7465
8447
  rollback. Disable or delete it when the re-signed build is verified.
7466
8448
  `;
8449
+ };
8450
+ const downloadProfileToTmp = (api, profileId) => Effect.gen(function* () {
8451
+ const data = yield* api.appleProvisioningProfiles.download({ path: { id: profileId } });
8452
+ const target = path.join(tmpdir(), `better-update-resign-${data.id}.mobileprovision`);
8453
+ yield* Effect.promise(async () => writeFile(target, fromBase64(data.profileBase64)));
8454
+ return {
8455
+ profilePath: target,
8456
+ profileName: data.profileName,
8457
+ bundleIdentifier: data.bundleIdentifier
8458
+ };
8459
+ });
8460
+ const resolveSigningIdentity = (api, certId) => Effect.gen(function* () {
8461
+ return `iPhone Distribution: ${(yield* api.appleDistributionCertificates.download({ path: { id: certId } })).appleTeamIdentifier}`;
8462
+ });
7467
8463
  const resignCommand = defineCommand({
7468
8464
  meta: {
7469
8465
  name: "resign",
7470
8466
  description: "Print step-by-step instructions for re-signing an iOS build with a new provisioning profile"
7471
8467
  },
7472
- args: { build: {
7473
- type: "string",
7474
- required: true,
7475
- description: "Source build ID"
7476
- } },
8468
+ args: {
8469
+ build: {
8470
+ type: "string",
8471
+ required: true,
8472
+ description: "Source build ID"
8473
+ },
8474
+ "profile-id": {
8475
+ type: "string",
8476
+ description: "Provisioning profile ID to bind to the resigned build (downloads it to a tmp path)"
8477
+ },
8478
+ "cert-id": {
8479
+ type: "string",
8480
+ description: "Distribution certificate ID to derive the codesign --signing-identity from"
8481
+ }
8482
+ },
7477
8483
  run: async ({ args }) => runEffect(Effect.gen(function* () {
7478
8484
  const api = yield* apiClient;
7479
8485
  const build = yield* api.builds.get({ path: { id: args.build } });
@@ -7483,7 +8489,19 @@ const resignCommand = defineCommand({
7483
8489
  return;
7484
8490
  }
7485
8491
  const link = yield* api.builds.getInstallLink({ path: { id: args.build } });
7486
- yield* printHuman(resignWorkflowText(args.build, link.artifactUrl));
8492
+ const profilePromise = args["profile-id"] === void 0 ? Effect.succeed(void 0) : downloadProfileToTmp(api, args["profile-id"]);
8493
+ const identityPromise = args["cert-id"] === void 0 ? Effect.succeed(void 0) : resolveSigningIdentity(api, args["cert-id"]);
8494
+ const [profile, identity] = yield* Effect.all([profilePromise, identityPromise], { concurrency: 2 });
8495
+ const resolved = profile === void 0 && identity === void 0 ? void 0 : {
8496
+ profilePath: profile?.profilePath ?? "/path/to/new.mobileprovision",
8497
+ signingIdentity: identity ?? "iPhone Distribution: Your Team (ABCDE12345)",
8498
+ appName: profile?.bundleIdentifier.split(".").pop() ?? "YourApp"
8499
+ };
8500
+ yield* printHuman(resignWorkflowText({
8501
+ buildId: args.build,
8502
+ installLink: link.artifactUrl,
8503
+ resolved
8504
+ }));
7487
8505
  }))
7488
8506
  });
7489
8507
 
@@ -7845,10 +8863,13 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
7845
8863
  if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
7846
8864
  const projectId = yield* extractProjectId(yield* readExpoConfig(projectRoot));
7847
8865
  const profile = yield* readBuildProfile(projectRoot, options.profileName);
7848
- const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, yield* pullEnvVars(api, {
7849
- projectId,
7850
- environment: profile.environment
7851
- })), options.platform);
8866
+ const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, {
8867
+ ...yield* pullEnvVars(api, {
8868
+ projectId,
8869
+ environment: profile.environment
8870
+ }),
8871
+ ...profile.env
8872
+ }), options.platform);
7852
8873
  const runtimeVersion = yield* resolveRuntimeVersion({
7853
8874
  raw: appMeta.rawRuntimeVersion,
7854
8875
  appVersion: appMeta.appVersion,
@@ -12957,6 +13978,195 @@ const statusCommand = defineCommand({
12957
13978
  }))
12958
13979
  });
12959
13980
 
13981
+ //#endregion
13982
+ //#region src/commands/submit/index.ts
13983
+ const PLATFORMS = ["ios", "android"];
13984
+ const resolveArchive = (api, projectId, platform, args) => Effect.gen(function* () {
13985
+ if (args.path !== void 0) return {
13986
+ archiveSource: "path",
13987
+ archiveUrl: args.path,
13988
+ buildId: void 0
13989
+ };
13990
+ if (args.url !== void 0) return {
13991
+ archiveSource: "url",
13992
+ archiveUrl: args.url,
13993
+ buildId: void 0
13994
+ };
13995
+ if (args.id !== void 0) return {
13996
+ archiveSource: "build",
13997
+ archiveUrl: (yield* api.builds.getInstallLink({ path: { id: args.id } })).artifactUrl,
13998
+ buildId: args.id
13999
+ };
14000
+ if (args.latest) {
14001
+ const { items } = yield* api.builds.list({ urlParams: {
14002
+ projectId,
14003
+ limit: 1,
14004
+ platform,
14005
+ sort: "-createdAt"
14006
+ } });
14007
+ const [latest] = items;
14008
+ if (latest === void 0) {
14009
+ yield* printHuman(`No builds found for platform ${platform}`);
14010
+ return null;
14011
+ }
14012
+ return {
14013
+ archiveSource: "build",
14014
+ archiveUrl: (yield* api.builds.getInstallLink({ path: { id: latest.id } })).artifactUrl,
14015
+ buildId: latest.id
14016
+ };
14017
+ }
14018
+ return null;
14019
+ });
14020
+ const buildIosCreatePayload = (iosProfile, whatToTest) => {
14021
+ if (iosProfile?.bundleIdentifier === void 0) return;
14022
+ return {
14023
+ bundleIdentifier: iosProfile.bundleIdentifier,
14024
+ ...iosProfile.appleId === void 0 ? {} : { appleId: iosProfile.appleId },
14025
+ ...iosProfile.ascAppId === void 0 ? {} : { ascAppId: iosProfile.ascAppId },
14026
+ ...iosProfile.appleTeamId === void 0 ? {} : { appleTeamId: iosProfile.appleTeamId },
14027
+ ...iosProfile.sku === void 0 ? {} : { sku: iosProfile.sku },
14028
+ ...iosProfile.language === void 0 ? {} : { language: iosProfile.language },
14029
+ ...iosProfile.companyName === void 0 ? {} : { companyName: iosProfile.companyName },
14030
+ ...iosProfile.appName === void 0 ? {} : { appName: iosProfile.appName },
14031
+ ...iosProfile.groups === void 0 ? {} : { groups: iosProfile.groups },
14032
+ ...whatToTest === void 0 ? {} : { whatToTest }
14033
+ };
14034
+ };
14035
+ const buildAndroidCreatePayload = (androidProfile) => {
14036
+ if (androidProfile?.applicationId === void 0) return;
14037
+ return {
14038
+ applicationId: androidProfile.applicationId,
14039
+ ...androidProfile.track === void 0 ? {} : { track: androidProfile.track },
14040
+ ...androidProfile.releaseStatus === void 0 ? {} : { releaseStatus: androidProfile.releaseStatus },
14041
+ ...androidProfile.changesNotSentForReview === void 0 ? {} : { changesNotSentForReview: androidProfile.changesNotSentForReview },
14042
+ ...androidProfile.rollout === void 0 ? {} : { rollout: androidProfile.rollout }
14043
+ };
14044
+ };
14045
+ const runFlow = (api, projectId, args) => Effect.gen(function* () {
14046
+ const iosConfig = buildIosCreatePayload(args.easProfile.ios, args.whatToTest);
14047
+ const androidConfig = buildAndroidCreatePayload(args.easProfile.android);
14048
+ const submission = yield* createSubmissionViaApi(api, {
14049
+ projectId,
14050
+ platform: args.platform,
14051
+ profileName: args.profile,
14052
+ archiveSource: args.archive.archiveSource,
14053
+ buildId: args.archive.buildId,
14054
+ archiveUrl: args.archive.archiveUrl,
14055
+ ...iosConfig === void 0 ? {} : { iosConfig },
14056
+ ...androidConfig === void 0 ? {} : { androidConfig }
14057
+ });
14058
+ yield* printHuman(`Submission created: ${submission.id} (${submission.status})`);
14059
+ if (args.platform === "ios" && iosConfig !== void 0) {
14060
+ const ascApiKeyId = args.easProfile.ios?.ascApiKeyId;
14061
+ if (ascApiKeyId === void 0) {
14062
+ yield* printHuman("iOS submission queued. Resolve ascApiKeyId in eas.json submit profile to enable client-side altool upload.");
14063
+ return submission;
14064
+ }
14065
+ yield* printHuman("Running xcrun altool upload locally...");
14066
+ yield* runIosAltoolUpload({
14067
+ api,
14068
+ submissionId: submission.id,
14069
+ ipaPath: args.archive.archiveUrl,
14070
+ ascApiKeyId
14071
+ });
14072
+ }
14073
+ if (args.platform === "android" && args.easProfile.android !== void 0) {
14074
+ yield* printHuman("Uploading bundle to Google Play locally...");
14075
+ const serviceAccountKeyId = args.serviceAccountKeyId ?? args.easProfile.android.serviceAccountKeyId;
14076
+ yield* runAndroidGooglePlayUpload({
14077
+ api,
14078
+ submissionId: submission.id,
14079
+ archive: {
14080
+ source: args.archive.archiveSource,
14081
+ value: args.archive.archiveUrl
14082
+ },
14083
+ androidProfile: args.easProfile.android,
14084
+ serviceAccountKeyId
14085
+ });
14086
+ }
14087
+ if (!args.wait) return submission;
14088
+ const terminal = yield* pollSubmissionUntilTerminal(api, submission.id);
14089
+ yield* printHuman(`Final status: ${terminal.status}`);
14090
+ if (terminal.errorCode !== null) yield* printHuman(`Error ${terminal.errorCode}: ${terminal.errorMessage ?? "(no detail)"}`);
14091
+ return terminal;
14092
+ });
14093
+ const submitCommand = defineCommand({
14094
+ meta: {
14095
+ name: "submit",
14096
+ description: "Submit a build to App Store Connect or Google Play"
14097
+ },
14098
+ args: {
14099
+ platform: {
14100
+ type: "enum",
14101
+ options: [...PLATFORMS],
14102
+ description: "Target platform"
14103
+ },
14104
+ profile: {
14105
+ type: "string",
14106
+ default: "production",
14107
+ description: "eas.json submit profile name (default: production)"
14108
+ },
14109
+ latest: {
14110
+ type: "boolean",
14111
+ description: "Submit the latest build for the platform"
14112
+ },
14113
+ id: {
14114
+ type: "string",
14115
+ description: "Submit a specific build by ID"
14116
+ },
14117
+ path: {
14118
+ type: "string",
14119
+ description: "Submit a local IPA/AAB at this path (URL or file://)"
14120
+ },
14121
+ url: {
14122
+ type: "string",
14123
+ description: "Submit a binary fetched from this URL"
14124
+ },
14125
+ "what-to-test": {
14126
+ type: "string",
14127
+ description: "iOS-only TestFlight changelog ('What to test')"
14128
+ },
14129
+ "service-account-key-id": {
14130
+ type: "string",
14131
+ description: "Android-only: better-update saved Google service account key ID (overrides eas.json submit profile)"
14132
+ },
14133
+ wait: {
14134
+ type: "boolean",
14135
+ default: true,
14136
+ description: "Block until submission reaches a terminal status (default: true)"
14137
+ }
14138
+ },
14139
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
14140
+ const { platform } = args;
14141
+ if (platform === void 0) {
14142
+ yield* printHuman("--platform is required (ios | android)");
14143
+ return;
14144
+ }
14145
+ const projectId = yield* readProjectId;
14146
+ const api = yield* apiClient;
14147
+ const easProfile = yield* resolveEasSubmitProfile((yield* readEasJson(process.cwd())).submit, args.profile);
14148
+ const archive = yield* resolveArchive(api, projectId, platform, {
14149
+ id: args.id,
14150
+ path: args.path,
14151
+ url: args.url,
14152
+ latest: args.latest ?? false
14153
+ });
14154
+ if (archive === null) {
14155
+ yield* printHuman("No archive resolved. Pass one of --latest, --id, --path, or --url.");
14156
+ return;
14157
+ }
14158
+ yield* runFlow(api, projectId, {
14159
+ platform,
14160
+ profile: args.profile,
14161
+ easProfile,
14162
+ archive,
14163
+ ...args["what-to-test"] === void 0 ? {} : { whatToTest: args["what-to-test"] },
14164
+ ...args["service-account-key-id"] === void 0 ? {} : { serviceAccountKeyId: args["service-account-key-id"] },
14165
+ wait: args.wait
14166
+ });
14167
+ }))
14168
+ });
14169
+
12960
14170
  //#endregion
12961
14171
  //#region src/commands/update/configure.ts
12962
14172
  const RUNTIME_POLICIES = ["appVersion", "fingerprint"];
@@ -15042,7 +16252,8 @@ await runMain(defineCommand({
15042
16252
  webhooks: webhooksCommand,
15043
16253
  autocomplete: autocompleteCommand,
15044
16254
  "migrate-config": migrateConfigCommand,
15045
- apple: appleCommand
16255
+ apple: appleCommand,
16256
+ submit: submitCommand
15046
16257
  }
15047
16258
  }));
15048
16259