@better-update/cli 0.28.1 → 0.29.1

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
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
34
 
35
35
  //#endregion
36
36
  //#region package.json
37
- var version = "0.28.1";
37
+ var version = "0.29.1";
38
38
 
39
39
  //#endregion
40
40
  //#region src/lib/interactive-mode.ts
@@ -904,7 +904,7 @@ var AssetsGroup = class extends HttpApiGroup.make("assets").add(HttpApiEndpoint.
904
904
 
905
905
  //#endregion
906
906
  //#region ../../packages/api/src/domain/audit-log.ts
907
- const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess", "policy", "group", "policyAttachment", "apiKey", "invitation", "member", "organization");
907
+ const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "environment", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess", "policy", "group", "policyAttachment", "apiKey", "invitation", "member", "organization");
908
908
  const AuditLogSource = Schema.Literal("session", "api-key");
909
909
  var AuditLog = class extends Schema.Class("AuditLog")({
910
910
  id: Id,
@@ -941,6 +941,7 @@ var Branch = class extends Schema.Class("Branch")({
941
941
  id: Id,
942
942
  projectId: Id,
943
943
  name: Schema.String,
944
+ isBuiltin: Schema.Boolean,
944
945
  createdAt: DateTimeString,
945
946
  updateCount: Schema.Number
946
947
  }) {};
@@ -1231,6 +1232,7 @@ var Channel = class extends Schema.Class("Channel")({
1231
1232
  branchMappingJson: Schema.NullOr(Schema.String),
1232
1233
  cacheVersion: Schema.Number,
1233
1234
  isPaused: Schema.Boolean,
1235
+ isBuiltin: Schema.Boolean,
1234
1236
  createdAt: DateTimeString
1235
1237
  }) {};
1236
1238
  const ChannelSortColumn = Schema.Literal("name", "createdAt");
@@ -1381,11 +1383,32 @@ var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoin
1381
1383
  description: "Apple device management for ad-hoc builds"
1382
1384
  })) {};
1383
1385
 
1386
+ //#endregion
1387
+ //#region ../../packages/api/src/domain/environment.ts
1388
+ /**
1389
+ * An environment name: lowercase letters, digits and hyphens, starting with a
1390
+ * letter. Shared by the env-var `environment` axis and the environment entity so
1391
+ * a custom environment name and an env var's `environment` use one shape.
1392
+ */
1393
+ const EnvironmentName = Schema.String.pipe(Schema.pattern(/^[a-z][a-z0-9-]*$/u), Schema.maxLength(64));
1394
+ /** An organization environment: a built-in (virtual) or a user-defined row. */
1395
+ var Environment = class extends Schema.Class("Environment")({
1396
+ id: Id,
1397
+ organizationId: Id,
1398
+ name: EnvironmentName,
1399
+ isBuiltin: Schema.Boolean,
1400
+ createdAt: DateTimeString
1401
+ }) {};
1402
+ const EnvironmentListResult = Schema.Struct({ items: Schema.Array(Environment) });
1403
+ const CreateEnvironmentBody = Schema.Struct({ name: EnvironmentName });
1404
+ const RenameEnvironmentBody = Schema.Struct({ name: EnvironmentName });
1405
+ const DeleteEnvironmentResult = Schema.Struct({ deleted: Schema.Number });
1406
+
1384
1407
  //#endregion
1385
1408
  //#region ../../packages/api/src/domain/env-var.ts
1386
1409
  const EnvVarVisibility = Schema.Literal("plaintext", "sensitive");
1387
1410
  const EnvVarScope = Schema.Literal("project", "global");
1388
- const EnvVarEnvironment = Schema.Literal("development", "preview", "production");
1411
+ const EnvVarEnvironment = EnvironmentName;
1389
1412
  const EnvVarListScope = Schema.Literal("all", "project", "global");
1390
1413
  /**
1391
1414
  * A client-sealed env var value. `id` is the revision UUID the CLI bound as the
@@ -1519,6 +1542,27 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1519
1542
  description: "Manage end-to-end encrypted, versioned environment variables for project builds"
1520
1543
  })) {};
1521
1544
 
1545
+ //#endregion
1546
+ //#region ../../packages/api/src/groups/environments.ts
1547
+ /** `:name` path parameter — the environment name (built-in or user-defined). */
1548
+ const nameParam = HttpApiSchema.param("name", Schema.String);
1549
+ var EnvironmentsGroup = class extends HttpApiGroup.make("environments").add(HttpApiEndpoint.get("list", "/api/environments").addSuccess(EnvironmentListResult).annotateContext(OpenApi.annotations({
1550
+ title: "List environments",
1551
+ description: "List the organization's environments: the three built-ins (development, preview, production) followed by user-defined ones."
1552
+ }))).add(HttpApiEndpoint.post("create", "/api/environments").setPayload(CreateEnvironmentBody).addSuccess(Environment, { status: 201 }).addError(Conflict).addError(BadRequest).annotateContext(OpenApi.annotations({
1553
+ title: "Create environment",
1554
+ description: "Create a user-defined environment for the organization. Built-in names are reserved."
1555
+ }))).add(HttpApiEndpoint.patch("rename")`/api/environments/${nameParam}`.setPayload(RenameEnvironmentBody).addSuccess(Environment).addError(Conflict).addError(BadRequest).annotateContext(OpenApi.annotations({
1556
+ title: "Rename environment",
1557
+ description: "Rename a user-defined environment. Built-ins cannot be renamed. Env vars referencing the old name are re-pointed at the new name."
1558
+ }))).add(HttpApiEndpoint.del("delete")`/api/environments/${nameParam}`.addSuccess(DeleteEnvironmentResult).addError(Conflict).annotateContext(OpenApi.annotations({
1559
+ title: "Delete environment",
1560
+ description: "Delete a user-defined environment. Built-ins cannot be deleted, nor can an environment still referenced by env vars."
1561
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
1562
+ title: "Environments",
1563
+ description: "Organization environment management endpoints"
1564
+ })) {};
1565
+
1522
1566
  //#endregion
1523
1567
  //#region ../../packages/api/src/domain/update.ts
1524
1568
  var Update = class extends Schema.Class("Update")({
@@ -2434,7 +2478,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
2434
2478
 
2435
2479
  //#endregion
2436
2480
  //#region ../../packages/api/src/api.ts
2437
- 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(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
2481
+ var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(EnvironmentsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
2438
2482
  title: "Better Update Management API",
2439
2483
  version: "1.0.0",
2440
2484
  description: "Management API for OTA update publishing, deployment, and analytics"
@@ -4789,7 +4833,7 @@ const parseLimit = (raw, defaultValue) => {
4789
4833
 
4790
4834
  //#endregion
4791
4835
  //#region src/commands/audit-logs/list.ts
4792
- const listCommand$9 = defineCommand({
4836
+ const listCommand$10 = defineCommand({
4793
4837
  meta: {
4794
4838
  name: "list",
4795
4839
  description: "List audit log entries"
@@ -4851,7 +4895,7 @@ const auditLogsCommand = defineCommand({
4851
4895
  name: "audit-logs",
4852
4896
  description: "View audit logs"
4853
4897
  },
4854
- subCommands: { list: listCommand$9 }
4898
+ subCommands: { list: listCommand$10 }
4855
4899
  });
4856
4900
 
4857
4901
  //#endregion
@@ -4955,7 +4999,7 @@ const drainPages = (fetchPage) => {
4955
4999
 
4956
5000
  //#endregion
4957
5001
  //#region src/commands/branches.ts
4958
- const listCommand$8 = defineCommand({
5002
+ const listCommand$9 = defineCommand({
4959
5003
  meta: {
4960
5004
  name: "list",
4961
5005
  description: "List branches for the linked project"
@@ -4978,7 +5022,7 @@ const listCommand$8 = defineCommand({
4978
5022
  ]), "No branches found.");
4979
5023
  }))
4980
5024
  });
4981
- const createCommand$4 = defineCommand({
5025
+ const createCommand$5 = defineCommand({
4982
5026
  meta: {
4983
5027
  name: "create",
4984
5028
  description: "Create a branch"
@@ -5033,7 +5077,7 @@ const viewCommand$3 = defineCommand({
5033
5077
  return branch;
5034
5078
  }), { json: "value" })
5035
5079
  });
5036
- const renameCommand$1 = defineCommand({
5080
+ const renameCommand$2 = defineCommand({
5037
5081
  meta: {
5038
5082
  name: "rename",
5039
5083
  description: "Rename a branch"
@@ -5059,7 +5103,7 @@ const renameCommand$1 = defineCommand({
5059
5103
  return branch;
5060
5104
  }), { json: "value" })
5061
5105
  });
5062
- const deleteCommand$6 = defineCommand({
5106
+ const deleteCommand$7 = defineCommand({
5063
5107
  meta: {
5064
5108
  name: "delete",
5065
5109
  description: "Delete a branch"
@@ -5084,11 +5128,11 @@ const branchesCommand = defineCommand({
5084
5128
  description: "Manage branches"
5085
5129
  },
5086
5130
  subCommands: {
5087
- list: listCommand$8,
5131
+ list: listCommand$9,
5088
5132
  view: viewCommand$3,
5089
- create: createCommand$4,
5090
- rename: renameCommand$1,
5091
- delete: deleteCommand$6
5133
+ create: createCommand$5,
5134
+ rename: renameCommand$2,
5135
+ delete: deleteCommand$7
5092
5136
  }
5093
5137
  });
5094
5138
 
@@ -18882,15 +18926,6 @@ const grantRecipient = (args) => Effect.gen(function* () {
18882
18926
  } });
18883
18927
  });
18884
18928
  /**
18885
- * Resolve the passphrase needed to unlock the active identity before a crypto
18886
- * operation: prompt for it when the identity is the on-disk file, or return
18887
- * `undefined` when the raw `BETTER_UPDATE_IDENTITY` env key is in use (CI). The
18888
- * resolved value is threaded into {@link unlockVaultKey} by the cipher helpers.
18889
- */
18890
- const resolveVaultPassphrase = Effect.gen(function* () {
18891
- return (yield* activeRecipient).source === "file" ? yield* promptPassword("Passphrase to unlock this device's identity:") : void 0;
18892
- });
18893
- /**
18894
18929
  * Unlock the org vault key for an interactive command, reusing a cached vault key
18895
18930
  * from the OS keychain when one is present and unexpired — so the device
18896
18931
  * passphrase is prompted at most once per cache TTL rather than on every command
@@ -18898,6 +18933,12 @@ const resolveVaultPassphrase = Effect.gen(function* () {
18898
18933
  * The CI `BETTER_UPDATE_IDENTITY` key carries no passphrase and is never cached:
18899
18934
  * it skips straight to the raw unwrap. On a cache miss the full unlock runs —
18900
18935
  * prompt, Argon2id, fetch + unwrap — and the result is cached for next time.
18936
+ *
18937
+ * The cached key is the unwrapped vault key, which both unwraps (decrypt/read)
18938
+ * and wraps (encrypt/write) DEKs — so this single entry point backs every vault
18939
+ * operation: download/build-resolve reads, seal-for-upload + generate writes, and
18940
+ * rotation. There is no read-only cache: an unlock makes the next write seamless
18941
+ * too.
18901
18942
  */
18902
18943
  const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
18903
18944
  const recipient = yield* activeRecipient;
@@ -18909,6 +18950,17 @@ const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
18909
18950
  yield* cache.set(recipient.publicKey, vault);
18910
18951
  return vault;
18911
18952
  }).pipe(Effect.provide(VaultCacheLive));
18953
+ /**
18954
+ * Forget the active recipient's cached vault key. Called after a rotation re-keys
18955
+ * the vault: the cached key + version are now stale, so leaving them would make
18956
+ * the next seal upload a key/version the server CAS-rejects (and the next decrypt
18957
+ * fail integrity). Clearing forces a fresh unlock at the new version next time —
18958
+ * which also correctly locks out a device that just revoked its own access.
18959
+ */
18960
+ const forgetCachedVaultKey = Effect.gen(function* () {
18961
+ const recipient = yield* activeRecipient;
18962
+ yield* (yield* VaultCache).clear(recipient.publicKey);
18963
+ }).pipe(Effect.provide(VaultCacheLive));
18912
18964
  /** Look up a registered recipient by its key id or full `SHA256:` fingerprint. */
18913
18965
  const findRecipient = (api, selector) => Effect.gen(function* () {
18914
18966
  const { items } = yield* api.userEncryptionKeys.list();
@@ -18948,13 +19000,6 @@ const getActiveOrgId = (api) => Effect.gen(function* () {
18948
19000
  if (me.activeOrganization === null) return yield* new IdentityError({ message: "No active organization for this token." });
18949
19001
  return me.activeOrganization.id;
18950
19002
  });
18951
- /** Resolve the active org id and unlock this device's vault key — the once-per-command I/O. */
18952
- const openVaultSession = (api, passphrase) => Effect.gen(function* () {
18953
- return {
18954
- orgId: yield* getActiveOrgId(api),
18955
- vault: yield* unlockVaultKey(api, passphrase)
18956
- };
18957
- });
18958
19003
  /**
18959
19004
  * {@link openVaultSession} that unlocks the vault key interactively — reusing the
18960
19005
  * OS-keychain-cached key when one is live (no prompt), prompting for the device
@@ -19566,7 +19611,7 @@ const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(funct
19566
19611
  ...compact({ validityDays: input.validityDays })
19567
19612
  });
19568
19613
  const bytes = yield* fs.readFile(keystorePath);
19569
- const session = yield* openVaultSession(api, input.passphrase ?? (yield* resolveVaultPassphrase));
19614
+ const session = yield* openVaultSessionInteractive(api);
19570
19615
  const metadata = { keyAlias: input.keyAlias };
19571
19616
  const envelope = yield* sealForUpload({
19572
19617
  session,
@@ -19617,7 +19662,7 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
19617
19662
  step: "p12-build",
19618
19663
  message: cause.message
19619
19664
  })));
19620
- const session = yield* openVaultSession(api, input.passphrase ?? (yield* resolveVaultPassphrase));
19665
+ const session = yield* openVaultSessionInteractive(api);
19621
19666
  const metadata = {
19622
19667
  serialNumber: bundle.metadata.serialNumber,
19623
19668
  appleTeamIdentifier: bundle.metadata.appleTeamId,
@@ -20623,7 +20668,7 @@ const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effec
20623
20668
  step: "parse-p12",
20624
20669
  message: cause.message
20625
20670
  })));
20626
- const session = yield* openVaultSession(api, yield* resolveVaultPassphrase);
20671
+ const session = yield* openVaultSessionInteractive(api);
20627
20672
  const envelopeMetadata = {
20628
20673
  serialNumber: metadata.serialNumber,
20629
20674
  appleTeamIdentifier: metadata.appleTeamId,
@@ -21021,14 +21066,12 @@ const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFo
21021
21066
  const randomKeystoreSecret = () => randomBytes(24).toString("base64url");
21022
21067
  const generateKeystoreAuto = (api, applicationIdentifier) => Effect.gen(function* () {
21023
21068
  yield* Console.log("Generating a new Android Keystore...");
21024
- const passphrase = yield* resolveVaultPassphrase;
21025
21069
  return (yield* generateAndUploadKeystore(api, {
21026
21070
  keyAlias: "upload",
21027
21071
  storePassword: randomKeystoreSecret(),
21028
21072
  keyPassword: randomKeystoreSecret(),
21029
21073
  commonName: applicationIdentifier,
21030
- organization: "better-update",
21031
- ...compact({ passphrase })
21074
+ organization: "better-update"
21032
21075
  })).id;
21033
21076
  });
21034
21077
  const generateKeystoreInteractive = (api) => Effect.gen(function* () {
@@ -21037,15 +21080,13 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
21037
21080
  const keyPassword = yield* promptPassword("Key password");
21038
21081
  const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
21039
21082
  const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
21040
- const passphrase = yield* resolveVaultPassphrase;
21041
21083
  yield* Console.log("Generating keystore with keytool...");
21042
21084
  return (yield* generateAndUploadKeystore(api, {
21043
21085
  keyAlias: alias,
21044
21086
  storePassword,
21045
21087
  keyPassword,
21046
21088
  commonName,
21047
- organization,
21048
- ...compact({ passphrase })
21089
+ organization
21049
21090
  })).id;
21050
21091
  });
21051
21092
  const pickExistingKeystore = (api) => Effect.gen(function* () {
@@ -21068,6 +21109,25 @@ const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
21068
21109
  })).id;
21069
21110
  });
21070
21111
  const resolveAndroidKeystoreId = (api, choice) => choice === "generate" ? generateKeystoreInteractive(api) : pickExistingKeystore(api);
21112
+ const bindAndroidKeystore = (api, appId, keystoreId) => Effect.gen(function* () {
21113
+ const existing = yield* api.androidBuildCredentials.list({ path: { applicationIdentifierId: appId } });
21114
+ const target = existing.items.find((group) => group.isDefault) ?? existing.items.at(0);
21115
+ if (target === void 0) {
21116
+ yield* api.androidBuildCredentials.create({
21117
+ path: { applicationIdentifierId: appId },
21118
+ payload: {
21119
+ name: "Default",
21120
+ isDefault: true,
21121
+ androidUploadKeystoreId: keystoreId
21122
+ }
21123
+ });
21124
+ return;
21125
+ }
21126
+ yield* api.androidBuildCredentials.update({
21127
+ path: { id: target.id },
21128
+ payload: { androidUploadKeystoreId: keystoreId }
21129
+ });
21130
+ });
21071
21131
  const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
21072
21132
  yield* Console.log("");
21073
21133
  yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
@@ -21090,15 +21150,7 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
21090
21150
  message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
21091
21151
  hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
21092
21152
  });
21093
- const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
21094
- yield* api.androidBuildCredentials.create({
21095
- path: { applicationIdentifierId: appId },
21096
- payload: {
21097
- name: "Default",
21098
- isDefault: true,
21099
- androidUploadKeystoreId: keystoreId
21100
- }
21101
- });
21153
+ yield* bindAndroidKeystore(api, appId, yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api));
21102
21154
  yield* Console.log("Android build credentials configured.");
21103
21155
  });
21104
21156
  const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
@@ -22734,7 +22786,8 @@ const warnIfDevClientMissing = (projectRoot) => Effect.gen(function* () {
22734
22786
 
22735
22787
  //#endregion
22736
22788
  //#region src/lib/env-exporter.ts
22737
- const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
22789
+ const ENVIRONMENT_NAME_PATTERN$1 = /^[a-z][a-z0-9-]*$/u;
22790
+ const coerceEnvironment = (raw) => ENVIRONMENT_NAME_PATTERN$1.test(raw) && raw.length <= 64 ? raw : void 0;
22738
22791
  /** Decrypt one sealed env var value, re-checking the sealed metadata against the row. */
22739
22792
  const decryptEnvVarValue = (session, item) => openFromDownload({
22740
22793
  session,
@@ -22766,7 +22819,7 @@ const exportDecryptedEnvVars = (api, projectId, environment) => Effect.gen(funct
22766
22819
  */
22767
22820
  const pullEnvVars = (api, { projectId, environment }) => Effect.gen(function* () {
22768
22821
  const validated = coerceEnvironment(environment);
22769
- if (!validated) return yield* new EnvExportError({ message: `Invalid environment "${environment}". Must be one of: development, preview, production.` });
22822
+ if (!validated) return yield* new EnvExportError({ message: `Invalid environment "${environment}": must be lowercase letters, digits, and hyphens, starting with a letter.` });
22770
22823
  const items = yield* exportDecryptedEnvVars(api, projectId, validated);
22771
22824
  return Object.fromEntries(items.map((item) => [item.key, item.value]));
22772
22825
  });
@@ -24536,7 +24589,7 @@ const compatibilityMatrixCommand = defineCommand({
24536
24589
 
24537
24590
  //#endregion
24538
24591
  //#region src/commands/builds/delete.ts
24539
- const deleteCommand$5 = defineCommand({
24592
+ const deleteCommand$6 = defineCommand({
24540
24593
  meta: {
24541
24594
  name: "delete",
24542
24595
  description: "Delete a build"
@@ -24682,7 +24735,7 @@ const DISTRIBUTION_OPTIONS$1 = [
24682
24735
  "play-store",
24683
24736
  "direct"
24684
24737
  ];
24685
- const listCommand$7 = defineCommand({
24738
+ const listCommand$8 = defineCommand({
24686
24739
  meta: {
24687
24740
  name: "list",
24688
24741
  description: "List builds for the linked project"
@@ -25340,9 +25393,9 @@ const buildsCommand = defineCommand({
25340
25393
  description: "Manage builds"
25341
25394
  },
25342
25395
  subCommands: {
25343
- list: listCommand$7,
25396
+ list: listCommand$8,
25344
25397
  get: getCommand$2,
25345
- delete: deleteCommand$5,
25398
+ delete: deleteCommand$6,
25346
25399
  download: downloadCommand$1,
25347
25400
  run: runCommand$1,
25348
25401
  "install-link": installLinkCommand,
@@ -25368,7 +25421,7 @@ const resolveNamedResourceId$1 = (params) => resolveNamedResourceId$2(params, (m
25368
25421
 
25369
25422
  //#endregion
25370
25423
  //#region src/commands/channels/create.ts
25371
- const createCommand$3 = defineCommand({
25424
+ const createCommand$4 = defineCommand({
25372
25425
  meta: {
25373
25426
  name: "create",
25374
25427
  description: "Create a channel"
@@ -25413,7 +25466,7 @@ const createCommand$3 = defineCommand({
25413
25466
 
25414
25467
  //#endregion
25415
25468
  //#region src/commands/channels/delete.ts
25416
- const deleteCommand$4 = defineCommand({
25469
+ const deleteCommand$5 = defineCommand({
25417
25470
  meta: {
25418
25471
  name: "delete",
25419
25472
  description: "Delete a channel"
@@ -25482,7 +25535,7 @@ const insightsCommand$1 = defineCommand({
25482
25535
 
25483
25536
  //#endregion
25484
25537
  //#region src/commands/channels/list.ts
25485
- const listCommand$6 = defineCommand({
25538
+ const listCommand$7 = defineCommand({
25486
25539
  meta: {
25487
25540
  name: "list",
25488
25541
  description: "List channels for the linked project"
@@ -25586,7 +25639,7 @@ const completeCommand$1 = defineCommand({
25586
25639
 
25587
25640
  //#endregion
25588
25641
  //#region src/commands/channels/rollout/create.ts
25589
- const createCommand$2 = defineCommand({
25642
+ const createCommand$3 = defineCommand({
25590
25643
  meta: {
25591
25644
  name: "create",
25592
25645
  description: "Start a branch rollout on a channel"
@@ -25706,7 +25759,7 @@ const rolloutCommand$1 = defineCommand({
25706
25759
  description: "Manage channel branch rollouts"
25707
25760
  },
25708
25761
  subCommands: {
25709
- create: createCommand$2,
25762
+ create: createCommand$3,
25710
25763
  update: updateCommand$3,
25711
25764
  complete: completeCommand$1,
25712
25765
  revert: revertCommand$2
@@ -25818,13 +25871,13 @@ const channelsCommand = defineCommand({
25818
25871
  description: "Manage channels"
25819
25872
  },
25820
25873
  subCommands: {
25821
- list: listCommand$6,
25874
+ list: listCommand$7,
25822
25875
  view: viewCommand$2,
25823
- create: createCommand$3,
25876
+ create: createCommand$4,
25824
25877
  update: updateCommand$2,
25825
25878
  pause: pauseCommand,
25826
25879
  resume: resumeCommand,
25827
- delete: deleteCommand$4,
25880
+ delete: deleteCommand$5,
25828
25881
  rollout: rolloutCommand$1,
25829
25882
  insights: insightsCommand$1
25830
25883
  }
@@ -26004,7 +26057,7 @@ const uploadIosDistributionCertificate = (api, input, bytes) => Effect.gen(funct
26004
26057
  validUntil: info.expiresAt.toISOString()
26005
26058
  };
26006
26059
  const envelope = yield* sealForUpload({
26007
- session: yield* openVaultSession(api, input.passphrase),
26060
+ session: yield* openVaultSessionInteractive(api),
26008
26061
  credentialType: "distribution-certificate",
26009
26062
  metadata,
26010
26063
  secret: {
@@ -26030,7 +26083,7 @@ const uploadIosPushKey = (api, input, bytes) => Effect.gen(function* () {
26030
26083
  appleTeamIdentifier: input.appleTeamIdentifier
26031
26084
  };
26032
26085
  const envelope = yield* sealForUpload({
26033
- session: yield* openVaultSession(api, input.passphrase),
26086
+ session: yield* openVaultSessionInteractive(api),
26034
26087
  credentialType: "push-key",
26035
26088
  metadata,
26036
26089
  secret: { p8Pem: toUtf8(bytes) }
@@ -26055,7 +26108,7 @@ const uploadIosAscApiKey = (api, input, bytes) => Effect.gen(function* () {
26055
26108
  appleTeamIdentifier: input.appleTeamIdentifier
26056
26109
  });
26057
26110
  const envelope = yield* sealForUpload({
26058
- session: yield* openVaultSession(api, input.passphrase),
26111
+ session: yield* openVaultSessionInteractive(api),
26059
26112
  credentialType: "asc-api-key",
26060
26113
  metadata,
26061
26114
  secret: { p8Pem: toUtf8(bytes) }
@@ -26089,7 +26142,7 @@ const uploadAndroidKeystore = (api, input, bytes) => Effect.gen(function* () {
26089
26142
  keyPassword: input.keyPassword
26090
26143
  })).keyAlias };
26091
26144
  const envelope = yield* sealForUpload({
26092
- session: yield* openVaultSession(api, input.passphrase),
26145
+ session: yield* openVaultSessionInteractive(api),
26093
26146
  credentialType: "keystore",
26094
26147
  metadata,
26095
26148
  secret: {
@@ -26117,7 +26170,7 @@ const uploadAndroidGoogleServiceAccountKey = (api, input, bytes) => Effect.gen(f
26117
26170
  googleProjectId: parsed.googleProjectId
26118
26171
  };
26119
26172
  const envelope = yield* sealForUpload({
26120
- session: yield* openVaultSession(api, input.passphrase),
26173
+ session: yield* openVaultSessionInteractive(api),
26121
26174
  credentialType: "google-service-account-key",
26122
26175
  metadata,
26123
26176
  secret: { json }
@@ -26146,10 +26199,7 @@ const uploadCredential = (api, input) => Effect.gen(function* () {
26146
26199
  const hasKey = (candidate) => Object.hasOwn(uploadHandlers, candidate);
26147
26200
  const handler = hasKey(key) ? uploadHandlers[key] : void 0;
26148
26201
  if (!handler) return yield* new CredentialValidationError({ message: `Unsupported credential combination: platform=${input.platform} type=${input.type}` });
26149
- return yield* handler(api, input.type === "provisioning-profile" || input.passphrase !== void 0 ? input : {
26150
- ...input,
26151
- ...compact({ passphrase: yield* resolveVaultPassphrase })
26152
- }, bytes);
26202
+ return yield* handler(api, input, bytes);
26153
26203
  });
26154
26204
  const deleteCredential = (api, input) => {
26155
26205
  const path = { id: input.id };
@@ -27140,10 +27190,14 @@ const runCredentialsManager = Effect.gen(function* () {
27140
27190
  * one, re-wrap the new vault key to each recipient, then submit the rotation
27141
27191
  * atomically (the server CAS-guards on the current version and requires a
27142
27192
  * recovery recipient in the set). Drops every recipient not in `recipients`.
27193
+ *
27194
+ * Unlocks via the cache-aware path (reusing a live `credentials unlock` session),
27195
+ * then drops that cached key once the re-key lands — it is now stale, so the next
27196
+ * operation must re-unlock at the new version.
27143
27197
  */
27144
27198
  const rotateVaultTo = (args) => Effect.gen(function* () {
27145
27199
  const orgId = yield* getActiveOrgId(args.api);
27146
- const current = yield* unlockVaultKey(args.api, args.passphrase);
27200
+ const current = yield* unlockVaultKeyInteractive(args.api);
27147
27201
  const newVaultKey = generateVaultKey();
27148
27202
  const newVersion = current.vaultVersion + 1;
27149
27203
  const { deks } = yield* args.api.orgVault.listCredentialDeks();
@@ -27181,11 +27235,13 @@ const rotateVaultTo = (args) => Effect.gen(function* () {
27181
27235
  recipient: recipient.publicKey
27182
27236
  }))
27183
27237
  })), { concurrency: "unbounded" });
27184
- return yield* args.api.orgVault.rotate({ payload: {
27238
+ const rotated = yield* args.api.orgVault.rotate({ payload: {
27185
27239
  fromVersion: current.vaultVersion,
27186
27240
  recipientWraps,
27187
27241
  credentialDeks
27188
27242
  } });
27243
+ yield* forgetCachedVaultKey;
27244
+ return rotated;
27189
27245
  });
27190
27246
  /** The encryption keys currently holding the vault key, joined with their public keys. */
27191
27247
  const currentRecipients = (api) => Effect.gen(function* () {
@@ -27239,7 +27295,7 @@ const toRecipientView = (userEncryptionKeyId, key) => ({
27239
27295
  fingerprint: key?.fingerprint
27240
27296
  })
27241
27297
  });
27242
- const listCommand$5 = defineCommand({
27298
+ const listCommand$6 = defineCommand({
27243
27299
  meta: {
27244
27300
  name: "list",
27245
27301
  description: "List recipients that currently hold the org vault key"
@@ -27320,7 +27376,6 @@ const rotateCommand = defineCommand({
27320
27376
  yield* confirmRecipients(recipients, args.yes === true);
27321
27377
  const rotated = yield* rotateVaultTo({
27322
27378
  api,
27323
- passphrase: yield* resolveVaultPassphrase,
27324
27379
  recipients: recipients.map(toRotationRecipient)
27325
27380
  });
27326
27381
  yield* printHuman(`Rotated the vault to version ${String(rotated.vaultVersion)} (${String(recipients.length)} recipients).`);
@@ -27356,7 +27411,6 @@ const revokeCommand$1 = defineCommand({
27356
27411
  yield* confirmRecipients(surviving, args.yes === true);
27357
27412
  const rotated = yield* rotateVaultTo({
27358
27413
  api,
27359
- passphrase: yield* resolveVaultPassphrase,
27360
27414
  recipients: surviving.map(toRotationRecipient)
27361
27415
  });
27362
27416
  yield* printHuman(`Revoked ${target.label} and rotated the vault to version ${String(rotated.vaultVersion)}.`);
@@ -27442,7 +27496,6 @@ const recoveryCommand = defineCommand({
27442
27496
  yield* confirmRecipients(surviving, args.yes === true);
27443
27497
  const rotated = yield* rotateVaultTo({
27444
27498
  api,
27445
- passphrase: yield* resolveVaultPassphrase,
27446
27499
  recipients: [...surviving.map(toRotationRecipient), {
27447
27500
  userEncryptionKeyId: registered.id,
27448
27501
  publicKey: newRecovery.publicKey
@@ -27466,7 +27519,7 @@ const accessCommand = defineCommand({
27466
27519
  description: "Inspect, grant, rotate, revoke, and recover access to the org credential vault"
27467
27520
  },
27468
27521
  subCommands: {
27469
- list: listCommand$5,
27522
+ list: listCommand$6,
27470
27523
  grant: grantCommand,
27471
27524
  rotate: rotateCommand,
27472
27525
  revoke: revokeCommand$1,
@@ -27675,7 +27728,7 @@ const CREDENTIAL_TYPES$3 = [
27675
27728
  "keystore",
27676
27729
  "google-service-account-key"
27677
27730
  ];
27678
- const deleteCommand$3 = defineCommand({
27731
+ const deleteCommand$4 = defineCommand({
27679
27732
  meta: {
27680
27733
  name: "delete",
27681
27734
  description: "Delete a credential"
@@ -27717,7 +27770,7 @@ const deleteCommand$3 = defineCommand({
27717
27770
  //#region src/commands/credentials/device.ts
27718
27771
  /** Self-linking is for your own device keys; recovery/machine keys go through `access grant`. */
27719
27772
  const requireDeviceKind = (target) => target.kind === "device" ? Effect.void : new IdentityError({ message: `Key ${target.id} is a ${target.kind} key, not a device. Use \`better-update credentials access grant\` for recovery/machine keys.` });
27720
- const listCommand$4 = defineCommand({
27773
+ const listCommand$5 = defineCommand({
27721
27774
  meta: {
27722
27775
  name: "list",
27723
27776
  description: "List your registered device keys (the active one is marked)"
@@ -27775,7 +27828,7 @@ const deviceCommand = defineCommand({
27775
27828
  description: "Manage your vault device keys"
27776
27829
  },
27777
27830
  subCommands: {
27778
- list: listCommand$4,
27831
+ list: listCommand$5,
27779
27832
  link: linkCommand
27780
27833
  },
27781
27834
  default: "list"
@@ -28430,7 +28483,7 @@ const printRecipient = (key) => printKeyValue([
28430
28483
  ["Recipient (public key)", key.publicKey],
28431
28484
  ["Fingerprint", key.fingerprint]
28432
28485
  ]);
28433
- const createCommand$1 = defineCommand({
28486
+ const createCommand$2 = defineCommand({
28434
28487
  meta: {
28435
28488
  name: "create",
28436
28489
  description: "Create this device's encryption identity and register it as a recipient"
@@ -28533,7 +28586,7 @@ const identityCommand = defineCommand({
28533
28586
  description: "Manage this device's end-to-end encryption identity"
28534
28587
  },
28535
28588
  subCommands: {
28536
- create: createCommand$1,
28589
+ create: createCommand$2,
28537
28590
  init: initCommand$1,
28538
28591
  register: registerCommand,
28539
28592
  show: showCommand
@@ -28543,7 +28596,7 @@ const identityCommand = defineCommand({
28543
28596
 
28544
28597
  //#endregion
28545
28598
  //#region src/commands/credentials/list.ts
28546
- const listCommand$3 = defineCommand({
28599
+ const listCommand$4 = defineCommand({
28547
28600
  meta: {
28548
28601
  name: "list",
28549
28602
  description: "List credentials across platforms"
@@ -29745,14 +29798,14 @@ const credentialsCommand = defineCommand({
29745
29798
  unlock: unlockCommand,
29746
29799
  lock: lockCommand,
29747
29800
  status: statusCommand$1,
29748
- list: listCommand$3,
29801
+ list: listCommand$4,
29749
29802
  view: viewCommand$1,
29750
29803
  download: downloadCommand,
29751
29804
  upload: uploadCommand,
29752
29805
  "upload-asc-key": uploadAscKeyCommand,
29753
29806
  generate: generateCommand$1,
29754
29807
  "regenerate-profile": regenerateProfileCommand,
29755
- delete: deleteCommand$3,
29808
+ delete: deleteCommand$4,
29756
29809
  remove: removeCommand,
29757
29810
  revoke: revokeCommand,
29758
29811
  configure: configureCommand$1,
@@ -30252,19 +30305,20 @@ const envErrorExtras = {
30252
30305
  SystemError: 6,
30253
30306
  BadArgument: 6
30254
30307
  };
30255
- const isEnvironmentName = (value) => value === "development" || value === "preview" || value === "production";
30308
+ const ENVIRONMENT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/u;
30309
+ const isEnvironmentName = (value) => ENVIRONMENT_NAME_PATTERN.test(value) && value.length <= 64;
30256
30310
  const parseEnvironmentsArg = (raw) => Effect.gen(function* () {
30257
30311
  const tokens = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
30258
- if (tokens.length === 0) return yield* new InvalidArgumentError({ message: "Provide at least one environment (development, preview, production)." });
30312
+ if (tokens.length === 0) return yield* new InvalidArgumentError({ message: "Provide at least one environment (e.g. development, preview, production)." });
30259
30313
  const seen = /* @__PURE__ */ new Set();
30260
30314
  yield* Effect.forEach(tokens, (token) => Effect.gen(function* () {
30261
- if (!isEnvironmentName(token)) return yield* new InvalidArgumentError({ message: `Invalid environment "${token}". Must be one of: development, preview, production.` });
30315
+ if (!isEnvironmentName(token)) return yield* new InvalidArgumentError({ message: `Invalid environment "${token}": must be lowercase letters, digits, and hyphens, starting with a letter.` });
30262
30316
  seen.add(token);
30263
30317
  }), { discard: true });
30264
30318
  return [...seen];
30265
30319
  });
30266
30320
  const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
30267
- if (!isEnvironmentName(raw)) return yield* new InvalidArgumentError({ message: `Invalid environment "${raw}". Must be one of: development, preview, production.` });
30321
+ if (!isEnvironmentName(raw)) return yield* new InvalidArgumentError({ message: `Invalid environment "${raw}": must be lowercase letters, digits, and hyphens, starting with a letter.` });
30268
30322
  return raw;
30269
30323
  });
30270
30324
  const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
@@ -30303,7 +30357,7 @@ const findProjectEnvVar = (api, projectId, key, environment) => Effect.gen(funct
30303
30357
 
30304
30358
  //#endregion
30305
30359
  //#region src/commands/env/delete.ts
30306
- const deleteCommand$2 = defineCommand({
30360
+ const deleteCommand$3 = defineCommand({
30307
30361
  meta: {
30308
30362
  name: "delete",
30309
30363
  description: "Delete a project env var (one environment, or every environment by default)"
@@ -30573,7 +30627,7 @@ const importCommand = defineCommand({
30573
30627
 
30574
30628
  //#endregion
30575
30629
  //#region src/commands/env/list.ts
30576
- const listCommand$2 = defineCommand({
30630
+ const listCommand$3 = defineCommand({
30577
30631
  meta: {
30578
30632
  name: "list",
30579
30633
  description: "List environment variable metadata. Values are end-to-end encrypted — read them with `env pull`, `env export`, or `env get`."
@@ -30920,11 +30974,11 @@ const envCommand = defineCommand({
30920
30974
  description: "Manage environment variables"
30921
30975
  },
30922
30976
  subCommands: {
30923
- list: listCommand$2,
30977
+ list: listCommand$3,
30924
30978
  get: getCommand$1,
30925
30979
  set: setCommand$1,
30926
30980
  update: updateCommand$1,
30927
- delete: deleteCommand$2,
30981
+ delete: deleteCommand$3,
30928
30982
  history: historyCommand,
30929
30983
  rollback: rollbackCommand$1,
30930
30984
  import: importCommand,
@@ -30935,6 +30989,93 @@ const envCommand = defineCommand({
30935
30989
  }
30936
30990
  });
30937
30991
 
30992
+ //#endregion
30993
+ //#region src/commands/environments.ts
30994
+ const listCommand$2 = defineCommand({
30995
+ meta: {
30996
+ name: "list",
30997
+ description: "List the organization's environments"
30998
+ },
30999
+ run: async () => runEffect(Effect.gen(function* () {
31000
+ const { items } = yield* (yield* apiClient).environments.list();
31001
+ yield* printList(["Name", "Built-in"], items.map((environment) => [environment.name, environment.isBuiltin ? "yes" : "no"]), "No environments found.");
31002
+ return items;
31003
+ }), { json: "value" })
31004
+ });
31005
+ const createCommand$1 = defineCommand({
31006
+ meta: {
31007
+ name: "create",
31008
+ description: "Create a user-defined environment"
31009
+ },
31010
+ args: { name: {
31011
+ type: "positional",
31012
+ required: true,
31013
+ description: "Environment name (lowercase letters, digits, hyphens)"
31014
+ } },
31015
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31016
+ const environment = yield* (yield* apiClient).environments.create({ payload: { name: args.name } });
31017
+ yield* printKeyValue([["Name", environment.name], ["Created", environment.createdAt]]);
31018
+ return environment;
31019
+ }), { json: "value" })
31020
+ });
31021
+ const renameCommand$1 = defineCommand({
31022
+ meta: {
31023
+ name: "rename",
31024
+ description: "Rename a user-defined environment"
31025
+ },
31026
+ args: {
31027
+ name: {
31028
+ type: "positional",
31029
+ required: true,
31030
+ description: "Current environment name"
31031
+ },
31032
+ to: {
31033
+ type: "string",
31034
+ required: true,
31035
+ description: "New environment name"
31036
+ }
31037
+ },
31038
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31039
+ const environment = yield* (yield* apiClient).environments.rename({
31040
+ path: { name: args.name },
31041
+ payload: { name: args.to }
31042
+ });
31043
+ yield* printHuman(`Environment renamed to "${environment.name}".`);
31044
+ return environment;
31045
+ }), { json: "value" })
31046
+ });
31047
+ const deleteCommand$2 = defineCommand({
31048
+ meta: {
31049
+ name: "delete",
31050
+ description: "Delete a user-defined environment"
31051
+ },
31052
+ args: { name: {
31053
+ type: "positional",
31054
+ required: true,
31055
+ description: "Environment name"
31056
+ } },
31057
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31058
+ yield* (yield* apiClient).environments.delete({ path: { name: args.name } });
31059
+ yield* printHuman(`Environment "${args.name}" deleted.`);
31060
+ return {
31061
+ name: args.name,
31062
+ deleted: true
31063
+ };
31064
+ }), { json: "value" })
31065
+ });
31066
+ const environmentsCommand = defineCommand({
31067
+ meta: {
31068
+ name: "environments",
31069
+ description: "Manage organization environments"
31070
+ },
31071
+ subCommands: {
31072
+ list: listCommand$2,
31073
+ create: createCommand$1,
31074
+ rename: renameCommand$1,
31075
+ delete: deleteCommand$2
31076
+ }
31077
+ });
31078
+
30938
31079
  //#endregion
30939
31080
  //#region src/commands/fingerprint/compare.ts
30940
31081
  /**
@@ -35468,6 +35609,7 @@ const commandRegistry = {
35468
35609
  groups: groupsCommand,
35469
35610
  branches: branchesCommand,
35470
35611
  channels: channelsCommand,
35612
+ environments: environmentsCommand,
35471
35613
  build: buildCommand,
35472
35614
  builds: buildsCommand,
35473
35615
  credentials: credentialsCommand,