@better-update/cli 0.43.0 → 0.44.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
@@ -35,7 +35,7 @@ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
35
35
 
36
36
  //#endregion
37
37
  //#region package.json
38
- var version = "0.43.0";
38
+ var version = "0.44.1";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -549,10 +549,32 @@ const VaultWrapInput = Schema.Struct({
549
549
  wrappedKey: Schema.String.pipe(Schema.minLength(1))
550
550
  });
551
551
  /**
552
- * Bootstrap the org vault on the first upload: the initial wrap rows, which must
553
- * include the uploader's own recipient and the offline recovery recipient.
552
+ * An env-vault recipient kind. Superset of the credentials-vault recipient kinds
553
+ * plus `account` — the per-user account key the browser unwraps the env vault
554
+ * with. `recipientId` references `user_encryption_keys.id` for the first three and
555
+ * `account_keys.id` for `account` (polymorphic; see migration 0071). Defined here
556
+ * (alongside `VaultWrapInput`) rather than in `env-vault.ts` so `BootstrapVaultBody`
557
+ * can carry env wraps without a circular import (`env-vault.ts` → `org-vault.ts`).
554
558
  */
555
- const BootstrapVaultBody = Schema.Struct({ wraps: Schema.Array(VaultWrapInput).pipe(Schema.minItems(1)) });
559
+ const EnvVaultRecipientKind = Schema.Literal("device", "recovery", "machine", "account");
560
+ /** One recipient's env-vault wrap row in a bootstrap / cutover / grant / rotate submission (age blob, base64). */
561
+ const EnvVaultWrapInput = Schema.Struct({
562
+ recipientKind: EnvVaultRecipientKind,
563
+ recipientId: Id,
564
+ wrappedKey: Schema.String.pipe(Schema.minLength(1))
565
+ });
566
+ /**
567
+ * Bootstrap the org vault: the initial credential-vault wrap rows AND the initial
568
+ * env-vault wrap rows, each of which must include the uploader's own recipient and
569
+ * the offline recovery recipient. Orgs are "born forked" — the env vault is set up
570
+ * at bootstrap (server stamps the cutover + env version), so a separate
571
+ * `env-vault migrate` step is never needed. `envWraps` is required: a client that
572
+ * cannot produce them is too old to bootstrap.
573
+ */
574
+ const BootstrapVaultBody = Schema.Struct({
575
+ wraps: Schema.Array(VaultWrapInput).pipe(Schema.minItems(1)),
576
+ envWraps: Schema.Array(EnvVaultWrapInput).pipe(Schema.minItems(1))
577
+ });
556
578
  /**
557
579
  * Add a single wrap row at the current vault version (grant another user, or
558
580
  * self-link your own device). Authz is enforced server-side; the wrap itself is
@@ -1936,13 +1958,6 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1936
1958
 
1937
1959
  //#endregion
1938
1960
  //#region ../../packages/api/src/domain/env-vault.ts
1939
- /**
1940
- * An env-vault recipient kind. Superset of the credentials-vault recipient kinds
1941
- * plus `account` — the per-user account key the browser unwraps the env vault
1942
- * with. `recipientId` references `user_encryption_keys.id` for the first three and
1943
- * `account_keys.id` for `account` (polymorphic; see migration 0071).
1944
- */
1945
- const EnvVaultRecipientKind = Schema.Literal("device", "recovery", "machine", "account");
1946
1961
  /** One wrap of the env-vault key to a recipient (an opaque `age` blob). */
1947
1962
  var OrgEnvVaultKeyWrap = class extends Schema.Class("OrgEnvVaultKeyWrap")({
1948
1963
  organizationId: Id,
@@ -1968,12 +1983,6 @@ const EnvVaultRecipients = Schema.Struct({
1968
1983
  envVaultVersion: VaultVersion,
1969
1984
  recipients: Schema.Array(EnvVaultRecipientRef)
1970
1985
  });
1971
- /** One recipient's wrap row in a cutover / grant / rotate submission (age blob, base64). */
1972
- const EnvVaultWrapInput = Schema.Struct({
1973
- recipientKind: EnvVaultRecipientKind,
1974
- recipientId: Id,
1975
- wrappedKey: Schema.String.pipe(Schema.minLength(1))
1976
- });
1977
1986
  /** Add a single env wrap at the current env version (grant or self-link). */
1978
1987
  const AddEnvVaultWrapBody = Schema.Struct({
1979
1988
  envVaultVersion: VaultVersion,
@@ -29379,8 +29388,8 @@ const createCommand$3 = defineCommand({
29379
29388
  accountKeyId: registered.id,
29380
29389
  agePublicKey: registered.agePublicKey
29381
29390
  });
29382
- yield* printKeyValue([["Account key fingerprint", registered.fingerprint], ["Env access", cutOver ? "granted (self-linked)" : "pending env-vault migration"]]);
29383
- yield* printHuman(cutOver ? "Account key enrolled. You can now unlock env values from the browser after a 2FA step-up." : "Account key enrolled. It gains env access once an admin runs `better-update credentials env-vault migrate`.");
29391
+ yield* printKeyValue([["Account key fingerprint", registered.fingerprint], ["Env access", cutOver ? "granted (self-linked)" : "pending vault not initialized"]]);
29392
+ yield* printHuman(cutOver ? "Account key enrolled. You can now unlock env values from the browser after a 2FA step-up." : "Account key enrolled. It gains env access once the org vault is initialized (`better-update credentials identity init`).");
29384
29393
  return {
29385
29394
  fingerprint: registered.fingerprint,
29386
29395
  envAccess: cutOver
@@ -29396,7 +29405,7 @@ const linkCommand$1 = defineCommand({
29396
29405
  const api = yield* apiClient;
29397
29406
  const own = yield* findOwnAccountKey(api);
29398
29407
  if (own === null) return yield* new IdentityError({ message: "No account key enrolled. Run `better-update credentials account create` first." });
29399
- if (!(yield* orgHasCutOver(api))) return yield* new IdentityError({ message: "This organization has no env vault yet. An admin must run `better-update credentials env-vault migrate` first." });
29408
+ if (!(yield* orgHasCutOver(api))) return yield* new IdentityError({ message: "This organization's vault is not initialized. Run `better-update credentials identity init` first." });
29400
29409
  yield* linkAccountKeyToEnv(api, {
29401
29410
  accountKeyId: own.id,
29402
29411
  agePublicKey: own.agePublicKey
@@ -30223,76 +30232,6 @@ const wrapEnvKeyToRecipients = (evKey, recipients) => Effect.forEach(recipients,
30223
30232
  }))
30224
30233
  })), { concurrency: "unbounded" });
30225
30234
 
30226
- //#endregion
30227
- //#region src/application/env-vault-cutover.ts
30228
- /**
30229
- * Every recipient the env key is wrapped to at cutover: the credentials vault's
30230
- * device/recovery/machine recipients (so upgraded CLIs keep env access via their
30231
- * device key) PLUS every member's account key (so the browser can unlock env). New
30232
- * account keys enrolled later self-link via `credentials account create`.
30233
- */
30234
- const collectEnvRecipients = (api) => Effect.gen(function* () {
30235
- const cvRecipients = yield* currentRecipients(api);
30236
- const { items: accounts } = yield* api.accountKeys.list();
30237
- return [...cvRecipients.map((key) => ({
30238
- recipientKind: key.kind,
30239
- recipientId: key.id,
30240
- recipient: key.publicKey
30241
- })), ...accounts.map((account) => ({
30242
- recipientKind: "account",
30243
- recipientId: account.id,
30244
- recipient: account.agePublicKey
30245
- }))];
30246
- });
30247
- /**
30248
- * Re-key every env DEK from the credentials vault key to the new env key. Env
30249
- * values live in the credentials vault until cutover, so the source set comes from
30250
- * `orgVault.listCredentialDeks` filtered to env rows (`credentialType` ===
30251
- * `envVarValue`); each is unwrapped under the credentials key and re-wrapped under
30252
- * the env key at version 1.
30253
- */
30254
- const rekeyEnvDeksToEnvVault = (api, params) => Effect.gen(function* () {
30255
- const { deks } = yield* api.orgVault.listCredentialDeks();
30256
- return yield* Effect.forEach(deks.filter((dek) => dek.credentialType === "envVarValue"), (dek) => Effect.try({
30257
- try: () => rekeyEnvDek({
30258
- orgId: params.orgId,
30259
- credentialId: dek.credentialId,
30260
- wrappedDek: dek.wrappedDek,
30261
- from: params.cvKey,
30262
- fromVersion: dek.vaultVersion,
30263
- fromKind: "credentials",
30264
- to: params.evKey,
30265
- toVersion: 1,
30266
- toKind: "env"
30267
- }),
30268
- catch: () => new IdentityError({ message: "Failed to re-key an env value during the migration — re-unlock the vault and retry." })
30269
- }), { concurrency: "unbounded" });
30270
- });
30271
- /**
30272
- * One-shot cutover: fork the org's env values into a separate env vault. Generate
30273
- * a fresh env key, wrap it to every recipient, re-key every env DEK from the
30274
- * credentials key to the env key, and submit it all atomically. The server
30275
- * compare-and-swaps on the cutover sentinel, so a re-run after a partial failure is
30276
- * safe (a second cutover is rejected `Conflict`). The credentials vault — and
30277
- * every signing credential — is untouched.
30278
- */
30279
- const cutoverEnvVault = (api) => Effect.gen(function* () {
30280
- const orgId = yield* getActiveOrgId(api);
30281
- if ((yield* api.orgVault.get().pipe(Effect.catchTag("NotFound", () => new IdentityError({ message: "This organization has no credential vault yet. Run `better-update credentials identity init` first." })))).envVaultCutoverAt !== null) return yield* new IdentityError({ message: "This organization's env vault is already migrated." });
30282
- const current = yield* unlockVaultKeyInteractive(api);
30283
- const evKey = generateVaultKey();
30284
- const wraps = yield* wrapEnvKeyToRecipients(evKey, yield* collectEnvRecipients(api));
30285
- const envDeks = yield* rekeyEnvDeksToEnvVault(api, {
30286
- orgId,
30287
- cvKey: current.vaultKey,
30288
- evKey
30289
- });
30290
- return yield* api.envVault.cutover({ payload: {
30291
- wraps,
30292
- envDeks
30293
- } });
30294
- });
30295
-
30296
30235
  //#endregion
30297
30236
  //#region src/application/env-vault-rotation.ts
30298
30237
  /**
@@ -30343,7 +30282,7 @@ const rekeyEnvDeksForRotation = (api, params) => Effect.gen(function* () {
30343
30282
  */
30344
30283
  const rotateEnvVault = (api) => Effect.gen(function* () {
30345
30284
  const orgId = yield* getActiveOrgId(api);
30346
- if ((yield* api.orgVault.get().pipe(Effect.catchTag("NotFound", () => new IdentityError({ message: "This organization has no credential vault yet." })))).envVaultCutoverAt === null) return yield* new IdentityError({ message: "This organization has no env vault yet run `better-update credentials env-vault migrate` first." });
30285
+ if ((yield* api.orgVault.get().pipe(Effect.catchTag("NotFound", () => new IdentityError({ message: "This organization has no credential vault yet." })))).envVaultCutoverAt === null) return yield* new IdentityError({ message: "This organization's env vault is not initialized." });
30347
30286
  const current = yield* unlockEnvVaultKeyInteractive(api);
30348
30287
  const toVersion = current.vaultVersion + 1;
30349
30288
  const newEvKey = generateVaultKey();
@@ -30365,33 +30304,6 @@ const rotateEnvVault = (api) => Effect.gen(function* () {
30365
30304
 
30366
30305
  //#endregion
30367
30306
  //#region src/commands/credentials/env-vault.ts
30368
- const migrateCommand = defineCommand({
30369
- meta: {
30370
- name: "migrate",
30371
- description: "Fork env values into a separate env vault so they can be unlocked from the browser (one-time, admin)"
30372
- },
30373
- args: { yes: {
30374
- type: "boolean",
30375
- description: "Skip the confirmation prompt"
30376
- } },
30377
- run: async ({ args }) => runEffect(Effect.gen(function* () {
30378
- const api = yield* apiClient;
30379
- yield* printHuman("Migrating splits env values into their own end-to-end-encrypted vault, wrapped to every device and account key.");
30380
- yield* printHuman("⚠ This is one-shot per organization. Until they upgrade, older CLIs can no longer READ env values (credentials are unaffected).");
30381
- yield* printHuman("⚠ Avoid env-var writes (set/push/import/update) elsewhere while this runs — a value written mid-migration may need re-setting afterwards.");
30382
- if (!(args.yes === true || (yield* promptConfirm("Migrate this organization's env values to a separate vault now?", { initialValue: false })))) {
30383
- yield* printHuman("Aborted — no changes made.");
30384
- return { migrated: false };
30385
- }
30386
- const vault = yield* cutoverEnvVault(api);
30387
- yield* printHuman(`✓ Env vault created (version ${String(vault.envVaultVersion)}).`);
30388
- yield* printHuman("Members enroll browser access with `better-update credentials account create`.");
30389
- return {
30390
- migrated: true,
30391
- envVaultVersion: vault.envVaultVersion
30392
- };
30393
- }), { json: "value" })
30394
- });
30395
30307
  const rotateCommand = defineCommand({
30396
30308
  meta: {
30397
30309
  name: "rotate",
@@ -30432,10 +30344,9 @@ const statusCommand$2 = defineCommand({
30432
30344
  const envVaultCommand = defineCommand({
30433
30345
  meta: {
30434
30346
  name: "env-vault",
30435
- description: "Manage the organization's separate env-vault (migrate, rotate, status)"
30347
+ description: "Manage the organization's env-vault (rotate, status)"
30436
30348
  },
30437
30349
  subCommands: {
30438
- migrate: migrateCommand,
30439
30350
  rotate: rotateCommand,
30440
30351
  status: statusCommand$2
30441
30352
  },
@@ -30956,13 +30867,16 @@ const RECOVERY_LABEL = "Offline recovery key";
30956
30867
  /**
30957
30868
  * Bootstrap the org vault on first use: generate the org vault key locally, mint
30958
30869
  * an offline recovery recipient, wrap the vault key to BOTH the caller's device
30959
- * and the recovery key, and POST the two initial wrap rows. The server requires
30960
- * the bootstrap to include a `recovery` recipient (break-glass), so both wraps go
30961
- * up together. Returns the unlocked vault key plus the recovery private key for a
30962
- * one-time, offline-only display.
30870
+ * and the recovery key, and POST the initial wrap rows. The org is "born forked" —
30871
+ * a second, INDEPENDENT env-vault key is generated and wrapped to the same device +
30872
+ * recovery recipients in the same call, so the env vault is the default and
30873
+ * `env-vault migrate` is never needed. The server requires both a `recovery`
30874
+ * recipient (break-glass) and the env wraps. Returns the unlocked vault key plus
30875
+ * the recovery private key for a one-time, offline-only display.
30963
30876
  */
30964
30877
  const bootstrapVault = (args) => Effect.gen(function* () {
30965
30878
  const vaultKey = generateVaultKey();
30879
+ const envVaultKey = generateVaultKey();
30966
30880
  const recovery = yield* Effect.promise(async () => generateIdentity());
30967
30881
  const recoveryKey = yield* args.api.userEncryptionKeys.register({ payload: {
30968
30882
  kind: "recovery",
@@ -30977,15 +30891,27 @@ const bootstrapVault = (args) => Effect.gen(function* () {
30977
30891
  vaultKey,
30978
30892
  recipient: recovery.publicKey
30979
30893
  }))]);
30894
+ const envWraps = yield* wrapEnvKeyToRecipients(envVaultKey, [{
30895
+ recipientKind: "device",
30896
+ recipientId: args.deviceKeyId,
30897
+ recipient: args.deviceRecipient
30898
+ }, {
30899
+ recipientKind: "recovery",
30900
+ recipientId: recoveryKey.id,
30901
+ recipient: recovery.publicKey
30902
+ }]);
30980
30903
  return {
30981
30904
  vaultKey,
30982
- vaultVersion: (yield* args.api.orgVault.bootstrap({ payload: { wraps: [{
30983
- userEncryptionKeyId: args.deviceKeyId,
30984
- wrappedKey: toBase64(deviceWrap)
30985
- }, {
30986
- userEncryptionKeyId: recoveryKey.id,
30987
- wrappedKey: toBase64(recoveryWrap)
30988
- }] } })).vaultVersion,
30905
+ vaultVersion: (yield* args.api.orgVault.bootstrap({ payload: {
30906
+ wraps: [{
30907
+ userEncryptionKeyId: args.deviceKeyId,
30908
+ wrappedKey: toBase64(deviceWrap)
30909
+ }, {
30910
+ userEncryptionKeyId: recoveryKey.id,
30911
+ wrappedKey: toBase64(recoveryWrap)
30912
+ }],
30913
+ envWraps
30914
+ } })).vaultVersion,
30989
30915
  keyId: args.deviceKeyId,
30990
30916
  recoveryPrivateKey: recovery.privateKey,
30991
30917
  recoveryFingerprint: recovery.fingerprint