@better-update/cli 0.29.0 → 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.29.0";
37
+ var version = "0.29.1";
38
38
 
39
39
  //#endregion
40
40
  //#region src/lib/interactive-mode.ts
@@ -18926,15 +18926,6 @@ const grantRecipient = (args) => Effect.gen(function* () {
18926
18926
  } });
18927
18927
  });
18928
18928
  /**
18929
- * Resolve the passphrase needed to unlock the active identity before a crypto
18930
- * operation: prompt for it when the identity is the on-disk file, or return
18931
- * `undefined` when the raw `BETTER_UPDATE_IDENTITY` env key is in use (CI). The
18932
- * resolved value is threaded into {@link unlockVaultKey} by the cipher helpers.
18933
- */
18934
- const resolveVaultPassphrase = Effect.gen(function* () {
18935
- return (yield* activeRecipient).source === "file" ? yield* promptPassword("Passphrase to unlock this device's identity:") : void 0;
18936
- });
18937
- /**
18938
18929
  * Unlock the org vault key for an interactive command, reusing a cached vault key
18939
18930
  * from the OS keychain when one is present and unexpired — so the device
18940
18931
  * passphrase is prompted at most once per cache TTL rather than on every command
@@ -18942,6 +18933,12 @@ const resolveVaultPassphrase = Effect.gen(function* () {
18942
18933
  * The CI `BETTER_UPDATE_IDENTITY` key carries no passphrase and is never cached:
18943
18934
  * it skips straight to the raw unwrap. On a cache miss the full unlock runs —
18944
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.
18945
18942
  */
18946
18943
  const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
18947
18944
  const recipient = yield* activeRecipient;
@@ -18953,6 +18950,17 @@ const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
18953
18950
  yield* cache.set(recipient.publicKey, vault);
18954
18951
  return vault;
18955
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));
18956
18964
  /** Look up a registered recipient by its key id or full `SHA256:` fingerprint. */
18957
18965
  const findRecipient = (api, selector) => Effect.gen(function* () {
18958
18966
  const { items } = yield* api.userEncryptionKeys.list();
@@ -18992,13 +19000,6 @@ const getActiveOrgId = (api) => Effect.gen(function* () {
18992
19000
  if (me.activeOrganization === null) return yield* new IdentityError({ message: "No active organization for this token." });
18993
19001
  return me.activeOrganization.id;
18994
19002
  });
18995
- /** Resolve the active org id and unlock this device's vault key — the once-per-command I/O. */
18996
- const openVaultSession = (api, passphrase) => Effect.gen(function* () {
18997
- return {
18998
- orgId: yield* getActiveOrgId(api),
18999
- vault: yield* unlockVaultKey(api, passphrase)
19000
- };
19001
- });
19002
19003
  /**
19003
19004
  * {@link openVaultSession} that unlocks the vault key interactively — reusing the
19004
19005
  * OS-keychain-cached key when one is live (no prompt), prompting for the device
@@ -19610,7 +19611,7 @@ const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(funct
19610
19611
  ...compact({ validityDays: input.validityDays })
19611
19612
  });
19612
19613
  const bytes = yield* fs.readFile(keystorePath);
19613
- const session = yield* openVaultSession(api, input.passphrase ?? (yield* resolveVaultPassphrase));
19614
+ const session = yield* openVaultSessionInteractive(api);
19614
19615
  const metadata = { keyAlias: input.keyAlias };
19615
19616
  const envelope = yield* sealForUpload({
19616
19617
  session,
@@ -19661,7 +19662,7 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
19661
19662
  step: "p12-build",
19662
19663
  message: cause.message
19663
19664
  })));
19664
- const session = yield* openVaultSession(api, input.passphrase ?? (yield* resolveVaultPassphrase));
19665
+ const session = yield* openVaultSessionInteractive(api);
19665
19666
  const metadata = {
19666
19667
  serialNumber: bundle.metadata.serialNumber,
19667
19668
  appleTeamIdentifier: bundle.metadata.appleTeamId,
@@ -20667,7 +20668,7 @@ const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effec
20667
20668
  step: "parse-p12",
20668
20669
  message: cause.message
20669
20670
  })));
20670
- const session = yield* openVaultSession(api, yield* resolveVaultPassphrase);
20671
+ const session = yield* openVaultSessionInteractive(api);
20671
20672
  const envelopeMetadata = {
20672
20673
  serialNumber: metadata.serialNumber,
20673
20674
  appleTeamIdentifier: metadata.appleTeamId,
@@ -21065,14 +21066,12 @@ const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFo
21065
21066
  const randomKeystoreSecret = () => randomBytes(24).toString("base64url");
21066
21067
  const generateKeystoreAuto = (api, applicationIdentifier) => Effect.gen(function* () {
21067
21068
  yield* Console.log("Generating a new Android Keystore...");
21068
- const passphrase = yield* resolveVaultPassphrase;
21069
21069
  return (yield* generateAndUploadKeystore(api, {
21070
21070
  keyAlias: "upload",
21071
21071
  storePassword: randomKeystoreSecret(),
21072
21072
  keyPassword: randomKeystoreSecret(),
21073
21073
  commonName: applicationIdentifier,
21074
- organization: "better-update",
21075
- ...compact({ passphrase })
21074
+ organization: "better-update"
21076
21075
  })).id;
21077
21076
  });
21078
21077
  const generateKeystoreInteractive = (api) => Effect.gen(function* () {
@@ -21081,15 +21080,13 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
21081
21080
  const keyPassword = yield* promptPassword("Key password");
21082
21081
  const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
21083
21082
  const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
21084
- const passphrase = yield* resolveVaultPassphrase;
21085
21083
  yield* Console.log("Generating keystore with keytool...");
21086
21084
  return (yield* generateAndUploadKeystore(api, {
21087
21085
  keyAlias: alias,
21088
21086
  storePassword,
21089
21087
  keyPassword,
21090
21088
  commonName,
21091
- organization,
21092
- ...compact({ passphrase })
21089
+ organization
21093
21090
  })).id;
21094
21091
  });
21095
21092
  const pickExistingKeystore = (api) => Effect.gen(function* () {
@@ -21112,6 +21109,25 @@ const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
21112
21109
  })).id;
21113
21110
  });
21114
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
+ });
21115
21131
  const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
21116
21132
  yield* Console.log("");
21117
21133
  yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
@@ -21134,15 +21150,7 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
21134
21150
  message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
21135
21151
  hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
21136
21152
  });
21137
- const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
21138
- yield* api.androidBuildCredentials.create({
21139
- path: { applicationIdentifierId: appId },
21140
- payload: {
21141
- name: "Default",
21142
- isDefault: true,
21143
- androidUploadKeystoreId: keystoreId
21144
- }
21145
- });
21153
+ yield* bindAndroidKeystore(api, appId, yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api));
21146
21154
  yield* Console.log("Android build credentials configured.");
21147
21155
  });
21148
21156
  const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
@@ -26049,7 +26057,7 @@ const uploadIosDistributionCertificate = (api, input, bytes) => Effect.gen(funct
26049
26057
  validUntil: info.expiresAt.toISOString()
26050
26058
  };
26051
26059
  const envelope = yield* sealForUpload({
26052
- session: yield* openVaultSession(api, input.passphrase),
26060
+ session: yield* openVaultSessionInteractive(api),
26053
26061
  credentialType: "distribution-certificate",
26054
26062
  metadata,
26055
26063
  secret: {
@@ -26075,7 +26083,7 @@ const uploadIosPushKey = (api, input, bytes) => Effect.gen(function* () {
26075
26083
  appleTeamIdentifier: input.appleTeamIdentifier
26076
26084
  };
26077
26085
  const envelope = yield* sealForUpload({
26078
- session: yield* openVaultSession(api, input.passphrase),
26086
+ session: yield* openVaultSessionInteractive(api),
26079
26087
  credentialType: "push-key",
26080
26088
  metadata,
26081
26089
  secret: { p8Pem: toUtf8(bytes) }
@@ -26100,7 +26108,7 @@ const uploadIosAscApiKey = (api, input, bytes) => Effect.gen(function* () {
26100
26108
  appleTeamIdentifier: input.appleTeamIdentifier
26101
26109
  });
26102
26110
  const envelope = yield* sealForUpload({
26103
- session: yield* openVaultSession(api, input.passphrase),
26111
+ session: yield* openVaultSessionInteractive(api),
26104
26112
  credentialType: "asc-api-key",
26105
26113
  metadata,
26106
26114
  secret: { p8Pem: toUtf8(bytes) }
@@ -26134,7 +26142,7 @@ const uploadAndroidKeystore = (api, input, bytes) => Effect.gen(function* () {
26134
26142
  keyPassword: input.keyPassword
26135
26143
  })).keyAlias };
26136
26144
  const envelope = yield* sealForUpload({
26137
- session: yield* openVaultSession(api, input.passphrase),
26145
+ session: yield* openVaultSessionInteractive(api),
26138
26146
  credentialType: "keystore",
26139
26147
  metadata,
26140
26148
  secret: {
@@ -26162,7 +26170,7 @@ const uploadAndroidGoogleServiceAccountKey = (api, input, bytes) => Effect.gen(f
26162
26170
  googleProjectId: parsed.googleProjectId
26163
26171
  };
26164
26172
  const envelope = yield* sealForUpload({
26165
- session: yield* openVaultSession(api, input.passphrase),
26173
+ session: yield* openVaultSessionInteractive(api),
26166
26174
  credentialType: "google-service-account-key",
26167
26175
  metadata,
26168
26176
  secret: { json }
@@ -26191,10 +26199,7 @@ const uploadCredential = (api, input) => Effect.gen(function* () {
26191
26199
  const hasKey = (candidate) => Object.hasOwn(uploadHandlers, candidate);
26192
26200
  const handler = hasKey(key) ? uploadHandlers[key] : void 0;
26193
26201
  if (!handler) return yield* new CredentialValidationError({ message: `Unsupported credential combination: platform=${input.platform} type=${input.type}` });
26194
- return yield* handler(api, input.type === "provisioning-profile" || input.passphrase !== void 0 ? input : {
26195
- ...input,
26196
- ...compact({ passphrase: yield* resolveVaultPassphrase })
26197
- }, bytes);
26202
+ return yield* handler(api, input, bytes);
26198
26203
  });
26199
26204
  const deleteCredential = (api, input) => {
26200
26205
  const path = { id: input.id };
@@ -27185,10 +27190,14 @@ const runCredentialsManager = Effect.gen(function* () {
27185
27190
  * one, re-wrap the new vault key to each recipient, then submit the rotation
27186
27191
  * atomically (the server CAS-guards on the current version and requires a
27187
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.
27188
27197
  */
27189
27198
  const rotateVaultTo = (args) => Effect.gen(function* () {
27190
27199
  const orgId = yield* getActiveOrgId(args.api);
27191
- const current = yield* unlockVaultKey(args.api, args.passphrase);
27200
+ const current = yield* unlockVaultKeyInteractive(args.api);
27192
27201
  const newVaultKey = generateVaultKey();
27193
27202
  const newVersion = current.vaultVersion + 1;
27194
27203
  const { deks } = yield* args.api.orgVault.listCredentialDeks();
@@ -27226,11 +27235,13 @@ const rotateVaultTo = (args) => Effect.gen(function* () {
27226
27235
  recipient: recipient.publicKey
27227
27236
  }))
27228
27237
  })), { concurrency: "unbounded" });
27229
- return yield* args.api.orgVault.rotate({ payload: {
27238
+ const rotated = yield* args.api.orgVault.rotate({ payload: {
27230
27239
  fromVersion: current.vaultVersion,
27231
27240
  recipientWraps,
27232
27241
  credentialDeks
27233
27242
  } });
27243
+ yield* forgetCachedVaultKey;
27244
+ return rotated;
27234
27245
  });
27235
27246
  /** The encryption keys currently holding the vault key, joined with their public keys. */
27236
27247
  const currentRecipients = (api) => Effect.gen(function* () {
@@ -27365,7 +27376,6 @@ const rotateCommand = defineCommand({
27365
27376
  yield* confirmRecipients(recipients, args.yes === true);
27366
27377
  const rotated = yield* rotateVaultTo({
27367
27378
  api,
27368
- passphrase: yield* resolveVaultPassphrase,
27369
27379
  recipients: recipients.map(toRotationRecipient)
27370
27380
  });
27371
27381
  yield* printHuman(`Rotated the vault to version ${String(rotated.vaultVersion)} (${String(recipients.length)} recipients).`);
@@ -27401,7 +27411,6 @@ const revokeCommand$1 = defineCommand({
27401
27411
  yield* confirmRecipients(surviving, args.yes === true);
27402
27412
  const rotated = yield* rotateVaultTo({
27403
27413
  api,
27404
- passphrase: yield* resolveVaultPassphrase,
27405
27414
  recipients: surviving.map(toRotationRecipient)
27406
27415
  });
27407
27416
  yield* printHuman(`Revoked ${target.label} and rotated the vault to version ${String(rotated.vaultVersion)}.`);
@@ -27487,7 +27496,6 @@ const recoveryCommand = defineCommand({
27487
27496
  yield* confirmRecipients(surviving, args.yes === true);
27488
27497
  const rotated = yield* rotateVaultTo({
27489
27498
  api,
27490
- passphrase: yield* resolveVaultPassphrase,
27491
27499
  recipients: [...surviving.map(toRotationRecipient), {
27492
27500
  userEncryptionKeyId: registered.id,
27493
27501
  publicKey: newRecovery.publicKey