@better-update/cli 0.43.0 → 0.44.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 +57 -131
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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.
|
|
38
|
+
var version = "0.44.0";
|
|
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
|
-
*
|
|
553
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
30960
|
-
*
|
|
30961
|
-
*
|
|
30962
|
-
*
|
|
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: {
|
|
30983
|
-
|
|
30984
|
-
|
|
30985
|
-
|
|
30986
|
-
|
|
30987
|
-
|
|
30988
|
-
|
|
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
|