@better-update/cli 0.22.0 → 0.23.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 +618 -250
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -33,7 +33,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.
|
|
36
|
+
var version = "0.23.0";
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region src/lib/interactive-mode.ts
|
|
@@ -356,11 +356,12 @@ const Ciphertext = Schema.String.pipe(Schema.minLength(1)).annotations({ descrip
|
|
|
356
356
|
/** Base64 of the per-credential DEK wrapped under the org vault key (AAD-bound). */
|
|
357
357
|
const WrappedDek = Schema.String.pipe(Schema.minLength(1)).annotations({ description: "Base64 of the DEK wrapped under the org vault key" });
|
|
358
358
|
/**
|
|
359
|
-
* The secret
|
|
360
|
-
*
|
|
361
|
-
*
|
|
359
|
+
* The secret kinds whose DEK is wrapped under the org vault key — the rows a
|
|
360
|
+
* rotation must re-wrap. Five signing-credential tables plus `envVarValue` (one
|
|
361
|
+
* row per environment variable value revision). Provisioning profiles are
|
|
362
|
+
* plaintext and are deliberately absent.
|
|
362
363
|
*/
|
|
363
|
-
const CredentialType = Schema.Literal("appleDistributionCertificate", "applePushKey", "ascApiKey", "googleServiceAccountKey", "androidUploadKeystore").annotations({ description: "Which encrypted-
|
|
364
|
+
const CredentialType = Schema.Literal("appleDistributionCertificate", "applePushKey", "ascApiKey", "googleServiceAccountKey", "androidUploadKeystore", "envVarValue").annotations({ description: "Which encrypted-secret table a vault-key DEK re-wrap targets" });
|
|
364
365
|
/**
|
|
365
366
|
* The client-encrypted envelope. Spread into each secret credential's upload
|
|
366
367
|
* body and download result alongside that credential's public metadata. The
|
|
@@ -383,6 +384,23 @@ const CredentialDekUpdate = Schema.Struct({
|
|
|
383
384
|
wrappedDek: WrappedDek
|
|
384
385
|
});
|
|
385
386
|
/**
|
|
387
|
+
* One credential's currently-stored wrapped DEK — what the client fetches before
|
|
388
|
+
* a rotation so it can unwrap each DEK with the old vault key and re-wrap it. The
|
|
389
|
+
* wrapped DEK is opaque (decryptable only with the vault key), so the server may
|
|
390
|
+
* serve it to any vault reader.
|
|
391
|
+
*/
|
|
392
|
+
const CredentialDekRef = Schema.Struct({
|
|
393
|
+
credentialType: CredentialType,
|
|
394
|
+
credentialId: Id,
|
|
395
|
+
wrappedDek: WrappedDek,
|
|
396
|
+
vaultVersion: VaultVersion
|
|
397
|
+
});
|
|
398
|
+
/** Every wrapped DEK in the org + the current vault version (the rotation source set). */
|
|
399
|
+
const VaultCredentialDeks = Schema.Struct({
|
|
400
|
+
vaultVersion: VaultVersion,
|
|
401
|
+
deks: Schema.Array(CredentialDekRef)
|
|
402
|
+
});
|
|
403
|
+
/**
|
|
386
404
|
* Rotate (or revoke) the org vault key. The client generates a new vault key at
|
|
387
405
|
* version `fromVersion + 1`, re-wraps every credential DEK under it, and
|
|
388
406
|
* re-wraps the new key to each surviving recipient (a revoke just omits the
|
|
@@ -1245,47 +1263,64 @@ const EnvVarVisibility = Schema.Literal("plaintext", "sensitive");
|
|
|
1245
1263
|
const EnvVarScope = Schema.Literal("project", "global");
|
|
1246
1264
|
const EnvVarEnvironment = Schema.Literal("development", "preview", "production");
|
|
1247
1265
|
const EnvVarListScope = Schema.Literal("all", "project", "global");
|
|
1266
|
+
/**
|
|
1267
|
+
* A client-sealed env var value. `id` is the revision UUID the CLI bound as the
|
|
1268
|
+
* AAD `credentialId` when sealing; the envelope fields are the opaque ciphertext,
|
|
1269
|
+
* wrapped DEK, and vault version. The server stores these and never decrypts —
|
|
1270
|
+
* env var values are end-to-end encrypted, like credentials.
|
|
1271
|
+
*/
|
|
1272
|
+
const EnvVarValueEnvelope = Schema.Struct({
|
|
1273
|
+
id: Id,
|
|
1274
|
+
...encryptedEnvelopeFields
|
|
1275
|
+
});
|
|
1276
|
+
/**
|
|
1277
|
+
* Env var metadata. The value is **not** here — it lives encrypted in the
|
|
1278
|
+
* revision pointed at by `currentRevisionId` and is only ever readable by the
|
|
1279
|
+
* CLI (which holds the org vault key). One entity per (scope, key, environment).
|
|
1280
|
+
*/
|
|
1248
1281
|
var EnvVar = class extends Schema.Class("EnvVar")({
|
|
1249
1282
|
id: Id,
|
|
1250
1283
|
organizationId: Id,
|
|
1251
1284
|
projectId: Schema.NullOr(Id),
|
|
1252
1285
|
scope: EnvVarScope,
|
|
1286
|
+
environment: EnvVarEnvironment,
|
|
1253
1287
|
key: Schema.String,
|
|
1254
1288
|
visibility: EnvVarVisibility,
|
|
1255
|
-
|
|
1256
|
-
|
|
1289
|
+
currentRevisionId: Schema.NullOr(Id),
|
|
1290
|
+
revisionNumber: Schema.NullOr(Schema.Number),
|
|
1291
|
+
revisionCount: Schema.Number,
|
|
1257
1292
|
overridesGlobal: Schema.optional(Schema.Boolean),
|
|
1258
1293
|
createdAt: DateTimeString,
|
|
1259
1294
|
updatedAt: DateTimeString
|
|
1260
1295
|
}) {};
|
|
1261
1296
|
const EnvVarKey = Schema.String.pipe(Schema.pattern(/^[A-Z][A-Z0-9_]*$/u), Schema.maxLength(256));
|
|
1262
|
-
const EnvVarValue = Schema.String.pipe(Schema.maxLength(32768));
|
|
1263
|
-
const EnvVarEnvironmentArray = Schema.Array(EnvVarEnvironment).pipe(Schema.minItems(1));
|
|
1264
1297
|
const CreateEnvVarBody = Schema.Struct({
|
|
1265
1298
|
scope: EnvVarScope,
|
|
1266
1299
|
projectId: Schema.optional(Id),
|
|
1267
|
-
|
|
1300
|
+
environment: EnvVarEnvironment,
|
|
1268
1301
|
key: EnvVarKey,
|
|
1269
|
-
|
|
1270
|
-
|
|
1302
|
+
visibility: EnvVarVisibility,
|
|
1303
|
+
value: EnvVarValueEnvelope
|
|
1271
1304
|
});
|
|
1272
1305
|
const UpdateEnvVarBody = Schema.Struct({
|
|
1273
|
-
value: Schema.optional(
|
|
1274
|
-
visibility: Schema.optional(EnvVarVisibility)
|
|
1275
|
-
environments: Schema.optional(EnvVarEnvironmentArray)
|
|
1306
|
+
value: Schema.optional(EnvVarValueEnvelope),
|
|
1307
|
+
visibility: Schema.optional(EnvVarVisibility)
|
|
1276
1308
|
});
|
|
1277
1309
|
const BulkImportEntry = Schema.Struct({
|
|
1278
1310
|
key: EnvVarKey,
|
|
1279
|
-
|
|
1280
|
-
visibility:
|
|
1311
|
+
environment: EnvVarEnvironment,
|
|
1312
|
+
visibility: EnvVarVisibility,
|
|
1313
|
+
value: EnvVarValueEnvelope
|
|
1281
1314
|
});
|
|
1315
|
+
/**
|
|
1316
|
+
* Bulk import already-sealed entries. The CLI parses the dotenv file, seals each
|
|
1317
|
+
* value per (key, environment) locally, and sends the envelopes — the server
|
|
1318
|
+
* cannot parse or encrypt plaintext itself.
|
|
1319
|
+
*/
|
|
1282
1320
|
const BulkImportEnvVarsBody = Schema.Struct({
|
|
1283
1321
|
scope: EnvVarScope,
|
|
1284
1322
|
projectId: Schema.optional(Id),
|
|
1285
|
-
|
|
1286
|
-
content: Schema.optional(Schema.String.pipe(Schema.maxLength(4e6))),
|
|
1287
|
-
entries: Schema.optional(Schema.Array(BulkImportEntry).pipe(Schema.maxItems(100))),
|
|
1288
|
-
visibility: Schema.optional(EnvVarVisibility)
|
|
1323
|
+
entries: Schema.Array(BulkImportEntry).pipe(Schema.minItems(1), Schema.maxItems(300))
|
|
1289
1324
|
});
|
|
1290
1325
|
const BulkImportResult = Schema.Struct({
|
|
1291
1326
|
created: Schema.Number,
|
|
@@ -1293,51 +1328,71 @@ const BulkImportResult = Schema.Struct({
|
|
|
1293
1328
|
skipped: Schema.Number
|
|
1294
1329
|
});
|
|
1295
1330
|
const DeleteEnvVarResult = Schema.Struct({ id: Id });
|
|
1331
|
+
/** One exported variable's sealed value envelope; the CLI decrypts it locally. */
|
|
1296
1332
|
const EnvVarExportItem = Schema.Struct({
|
|
1297
1333
|
key: Schema.String,
|
|
1298
|
-
|
|
1299
|
-
visibility: EnvVarVisibility
|
|
1334
|
+
environment: EnvVarEnvironment,
|
|
1335
|
+
visibility: EnvVarVisibility,
|
|
1336
|
+
id: Id,
|
|
1337
|
+
...encryptedEnvelopeFields
|
|
1300
1338
|
});
|
|
1301
1339
|
const EnvVarExportResult = Schema.Struct({
|
|
1302
1340
|
items: Schema.Array(EnvVarExportItem),
|
|
1303
1341
|
environment: EnvVarEnvironment
|
|
1304
1342
|
});
|
|
1343
|
+
/** One entry in a variable's value history (metadata only — no ciphertext). */
|
|
1344
|
+
const EnvVarRevision = Schema.Struct({
|
|
1345
|
+
id: Id,
|
|
1346
|
+
revisionNumber: Schema.Number,
|
|
1347
|
+
vaultVersion: VaultVersion,
|
|
1348
|
+
isCurrent: Schema.Boolean,
|
|
1349
|
+
createdBy: Schema.NullOr(Id),
|
|
1350
|
+
createdAt: DateTimeString
|
|
1351
|
+
});
|
|
1352
|
+
const EnvVarRevisionsResult = Schema.Struct({ items: Schema.Array(EnvVarRevision) });
|
|
1353
|
+
const RollbackEnvVarBody = Schema.Struct({ toRevisionId: Id });
|
|
1305
1354
|
|
|
1306
1355
|
//#endregion
|
|
1307
1356
|
//#region ../../packages/api/src/groups/env-vars.ts
|
|
1308
|
-
var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoint.post("create", "/api/env-vars").setPayload(CreateEnvVarBody).addSuccess(EnvVar, { status: 201 }).
|
|
1357
|
+
var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoint.post("create", "/api/env-vars").setPayload(CreateEnvVarBody).addSuccess(EnvVar, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1309
1358
|
title: "Create environment variable",
|
|
1310
|
-
description: "Create a new environment variable. Scope can be 'project' (requires projectId) or 'global'
|
|
1359
|
+
description: "Create a new environment variable for one environment. The body carries the client-sealed value envelope; the server never sees plaintext. Scope can be 'project' (requires projectId) or 'global'."
|
|
1311
1360
|
}))).add(HttpApiEndpoint.get("list", "/api/env-vars").setUrlParams(Schema.Struct({
|
|
1312
1361
|
scope: Schema.optional(EnvVarListScope),
|
|
1313
1362
|
projectId: Schema.optional(Id),
|
|
1314
1363
|
environments: Schema.optional(Schema.String),
|
|
1315
1364
|
search: Schema.optional(Schema.String),
|
|
1316
1365
|
...PaginationParams.fields
|
|
1317
|
-
})).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).
|
|
1366
|
+
})).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).annotateContext(OpenApi.annotations({
|
|
1318
1367
|
title: "List environment variables",
|
|
1319
|
-
description: "List environment
|
|
1368
|
+
description: "List environment variable metadata (no values — those are encrypted). scope=all merges project + global vars with project overrides. environments is a comma-separated list. search matches key substring."
|
|
1320
1369
|
}))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
|
|
1321
1370
|
title: "Get environment variable",
|
|
1322
|
-
description: "Get an environment variable by ID"
|
|
1323
|
-
}))).add(HttpApiEndpoint.patch("update")`/api/env-vars/${idParam}`.setPayload(UpdateEnvVarBody).addSuccess(EnvVar).
|
|
1371
|
+
description: "Get an environment variable's metadata by ID (no value)"
|
|
1372
|
+
}))).add(HttpApiEndpoint.patch("update")`/api/env-vars/${idParam}`.setPayload(UpdateEnvVarBody).addSuccess(EnvVar).annotateContext(OpenApi.annotations({
|
|
1324
1373
|
title: "Update environment variable",
|
|
1325
|
-
description: "
|
|
1374
|
+
description: "Change the value (a new sealed revision) and/or the visibility tier. The environment is immutable."
|
|
1326
1375
|
}))).add(HttpApiEndpoint.del("delete")`/api/env-vars/${idParam}`.addSuccess(DeleteEnvVarResult).annotateContext(OpenApi.annotations({
|
|
1327
1376
|
title: "Delete environment variable",
|
|
1328
|
-
description: "Delete an environment variable"
|
|
1329
|
-
}))).add(HttpApiEndpoint.
|
|
1377
|
+
description: "Delete an environment variable and all of its value revisions"
|
|
1378
|
+
}))).add(HttpApiEndpoint.get("revisions")`/api/env-vars/${idParam}/revisions`.addSuccess(EnvVarRevisionsResult).annotateContext(OpenApi.annotations({
|
|
1379
|
+
title: "List value revisions",
|
|
1380
|
+
description: "List a variable's value history (metadata only, newest first)"
|
|
1381
|
+
}))).add(HttpApiEndpoint.post("rollback")`/api/env-vars/${idParam}/rollback`.setPayload(RollbackEnvVarBody).addSuccess(EnvVar).annotateContext(OpenApi.annotations({
|
|
1382
|
+
title: "Roll back to a revision",
|
|
1383
|
+
description: "Re-point the active value at an earlier revision of this variable"
|
|
1384
|
+
}))).add(HttpApiEndpoint.post("bulkImport", "/api/env-vars/bulk-import").setPayload(BulkImportEnvVarsBody).addSuccess(BulkImportResult).annotateContext(OpenApi.annotations({
|
|
1330
1385
|
title: "Bulk import environment variables",
|
|
1331
|
-
description: "
|
|
1386
|
+
description: "Upsert pre-sealed entries (one per key+environment). The CLI parses the dotenv file and seals each value locally before sending."
|
|
1332
1387
|
}))).add(HttpApiEndpoint.get("export", "/api/env-vars/export").setUrlParams(Schema.Struct({
|
|
1333
1388
|
projectId: Id,
|
|
1334
1389
|
environment: EnvVarEnvironment
|
|
1335
1390
|
})).addSuccess(EnvVarExportResult).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1336
1391
|
title: "Export environment variables",
|
|
1337
|
-
description: "Export
|
|
1338
|
-
}))).addError(NotFound).addError(Forbidden).addError(BadRequest).annotateContext(OpenApi.annotations({
|
|
1392
|
+
description: "Export sealed value envelopes for a project environment (CLI decrypts locally). Global org-scoped vars are merged in; project values override globals on key collision. Bearer (CLI/API-key) auth only."
|
|
1393
|
+
}))).addError(NotFound).addError(Forbidden).addError(BadRequest).addError(Conflict).annotateContext(OpenApi.annotations({
|
|
1339
1394
|
title: "Environment Variables",
|
|
1340
|
-
description: "Manage environment variables for project builds
|
|
1395
|
+
description: "Manage end-to-end encrypted, versioned environment variables for project builds"
|
|
1341
1396
|
})) {};
|
|
1342
1397
|
|
|
1343
1398
|
//#endregion
|
|
@@ -1656,6 +1711,9 @@ var OrgVaultGroup = class extends HttpApiGroup.make("orgVault").add(HttpApiEndpo
|
|
|
1656
1711
|
}))).add(HttpApiEndpoint.get("getWrap")`/api/vault/wraps/${keyIdParam}`.addSuccess(RecipientVaultKey).annotateContext(OpenApi.annotations({
|
|
1657
1712
|
title: "Get vault wrap",
|
|
1658
1713
|
description: "Fetch the wrapped vault key for a recipient to unwrap locally"
|
|
1714
|
+
}))).add(HttpApiEndpoint.get("listCredentialDeks", "/api/vault/credential-deks").addSuccess(VaultCredentialDeks).annotateContext(OpenApi.annotations({
|
|
1715
|
+
title: "List wrapped credential DEKs",
|
|
1716
|
+
description: "Every wrapped DEK in the org + the current vault version — the client fetches these to re-wrap under a new vault key during a rotation (the DEKs are opaque)"
|
|
1659
1717
|
}))).add(HttpApiEndpoint.post("rotate", "/api/vault/rotate").setPayload(RotateVaultBody).addSuccess(OrgVault).annotateContext(OpenApi.annotations({
|
|
1660
1718
|
title: "Rotate vault key",
|
|
1661
1719
|
description: "Revoke or rotate (admin): bump the vault version, re-wrap every credential DEK, and re-wrap the new key to the surviving recipients — applied atomically with compare-and-swap"
|
|
@@ -2683,7 +2741,6 @@ const CALLBACK_PAGE = `<!doctype html>
|
|
|
2683
2741
|
}
|
|
2684
2742
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
2685
2743
|
render("CLI login complete. You can close this tab.");
|
|
2686
|
-
setTimeout(() => window.close(), 300);
|
|
2687
2744
|
})
|
|
2688
2745
|
.catch((error) => {
|
|
2689
2746
|
render(error instanceof Error ? error.message : "Callback failed.");
|
|
@@ -2776,6 +2833,7 @@ const createBrowserLoginServer = async (options = {}) => {
|
|
|
2776
2833
|
waitForToken: session.waitForToken,
|
|
2777
2834
|
stop: () => {
|
|
2778
2835
|
session.dispose();
|
|
2836
|
+
server.closeAllConnections();
|
|
2779
2837
|
server.close();
|
|
2780
2838
|
}
|
|
2781
2839
|
};
|
|
@@ -20713,14 +20771,19 @@ const unlockActivePrivateKey = (passphrase) => Effect.gen(function* () {
|
|
|
20713
20771
|
})).privateKey;
|
|
20714
20772
|
});
|
|
20715
20773
|
/**
|
|
20716
|
-
*
|
|
20717
|
-
* whether the org vault exists
|
|
20718
|
-
*
|
|
20719
|
-
*
|
|
20720
|
-
*
|
|
20774
|
+
* Actionable guidance for a device that can't reach the vault, branched on
|
|
20775
|
+
* whether the org vault exists yet. A fresh org has no vault — the first member
|
|
20776
|
+
* runs `credentials identity init` (which also mints the offline recovery key);
|
|
20777
|
+
* an existing vault means this device simply isn't a recipient, so it needs an
|
|
20778
|
+
* admin grant or a self-link from a device that already has it. Exported so the
|
|
20779
|
+
* post-`identity create` hint stays in sync with this error path.
|
|
20721
20780
|
*/
|
|
20781
|
+
const VAULT_NOT_RECIPIENT_GUIDANCE = "This device isn't a vault recipient yet. Ask an org admin to run `better-update credentials access grant`, or self-link from a device that already has access with `better-update credentials device link`.";
|
|
20782
|
+
const VAULT_NOT_SET_UP_GUIDANCE = "This organization's credential vault isn't set up yet. Run `better-update credentials identity init` to bootstrap it — you'll get a one-time offline recovery key to store safely.";
|
|
20783
|
+
/** `true` if the org vault has been bootstrapped; `false` on `NotFound` (a fresh org). */
|
|
20784
|
+
const orgVaultExists = (api) => api.orgVault.get().pipe(Effect.as(true), Effect.catchTag("NotFound", () => Effect.succeed(false)));
|
|
20722
20785
|
const vaultAccessError = (api) => Effect.gen(function* () {
|
|
20723
|
-
return yield* new IdentityError({ message: (yield* api
|
|
20786
|
+
return yield* new IdentityError({ message: (yield* orgVaultExists(api)) ? VAULT_NOT_RECIPIENT_GUIDANCE : VAULT_NOT_SET_UP_GUIDANCE });
|
|
20724
20787
|
});
|
|
20725
20788
|
/**
|
|
20726
20789
|
* Unlock the org vault key for this device: find this device's recipient row,
|
|
@@ -24314,18 +24377,76 @@ const warnIfDevClientMissing = (projectRoot) => Effect.gen(function* () {
|
|
|
24314
24377
|
//#endregion
|
|
24315
24378
|
//#region src/lib/env-exporter.ts
|
|
24316
24379
|
const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
|
|
24380
|
+
/** Decrypt one sealed env var value, re-checking the sealed metadata against the row. */
|
|
24381
|
+
const decryptEnvVarValue = (session, item) => openFromDownload({
|
|
24382
|
+
session,
|
|
24383
|
+
credentialType: "envVarValue",
|
|
24384
|
+
downloaded: item
|
|
24385
|
+
}).pipe(Effect.mapError((cause) => new EnvExportError({ message: `Failed to decrypt env var "${item.key}": ${cause.message}` })), Effect.flatMap((secret) => requireSecretString(secret, "value", () => new EnvExportError({ message: `Decrypted env var "${item.key}" is missing its value.` }))));
|
|
24386
|
+
/** Decrypt a batch of sealed env vars into plaintext key/value/visibility entries. */
|
|
24387
|
+
const decryptEnvVars = (session, items) => Effect.forEach(items, (item) => Effect.map(decryptEnvVarValue(session, item), (value) => ({
|
|
24388
|
+
key: item.key,
|
|
24389
|
+
value,
|
|
24390
|
+
visibility: item.visibility
|
|
24391
|
+
})), { concurrency: 8 });
|
|
24392
|
+
/**
|
|
24393
|
+
* Export and decrypt every env var for a project + environment, in key order.
|
|
24394
|
+
* Skips unlocking the vault when the project has no variables; otherwise unlocks
|
|
24395
|
+
* once and decrypts every value locally (the server never sees plaintext).
|
|
24396
|
+
*/
|
|
24397
|
+
const exportDecryptedEnvVars = (api, projectId, environment) => Effect.gen(function* () {
|
|
24398
|
+
const result = yield* api["env-vars"].export({ urlParams: {
|
|
24399
|
+
projectId,
|
|
24400
|
+
environment
|
|
24401
|
+
} }).pipe(Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
|
|
24402
|
+
if (result.items.length === 0) return [];
|
|
24403
|
+
return yield* decryptEnvVars(yield* openVaultSessionInteractive(api).pipe(Effect.mapError((cause) => new EnvExportError({ message: `Could not unlock the credential vault to decrypt environment variables: ${cause.message}` }))), result.items);
|
|
24404
|
+
});
|
|
24317
24405
|
/**
|
|
24318
|
-
* Pull environment variables
|
|
24319
|
-
* a
|
|
24406
|
+
* Pull + decrypt environment variables flattened into a key/value map for
|
|
24407
|
+
* injection into a build/subprocess.
|
|
24320
24408
|
*/
|
|
24321
|
-
const pullEnvVars = (api, { projectId, environment }) => {
|
|
24409
|
+
const pullEnvVars = (api, { projectId, environment }) => Effect.gen(function* () {
|
|
24322
24410
|
const validated = coerceEnvironment(environment);
|
|
24323
|
-
if (!validated) return
|
|
24324
|
-
|
|
24325
|
-
|
|
24326
|
-
|
|
24327
|
-
|
|
24328
|
-
|
|
24411
|
+
if (!validated) return yield* new EnvExportError({ message: `Invalid environment "${environment}". Must be one of: development, preview, production.` });
|
|
24412
|
+
const items = yield* exportDecryptedEnvVars(api, projectId, validated);
|
|
24413
|
+
return Object.fromEntries(items.map((item) => [item.key, item.value]));
|
|
24414
|
+
});
|
|
24415
|
+
/**
|
|
24416
|
+
* Seal each entry for every target environment and bulk-upsert them. Unlocks the
|
|
24417
|
+
* vault once; the server stores only the sealed envelopes (never the plaintext).
|
|
24418
|
+
*/
|
|
24419
|
+
const uploadEnvVars = (api, params) => Effect.gen(function* () {
|
|
24420
|
+
const session = yield* openVaultSessionInteractive(api);
|
|
24421
|
+
const pairs = params.environments.flatMap((environment) => params.entries.map((entry) => ({
|
|
24422
|
+
...entry,
|
|
24423
|
+
environment
|
|
24424
|
+
})));
|
|
24425
|
+
const sealed = yield* Effect.forEach(pairs, (pair) => Effect.map(sealForUpload({
|
|
24426
|
+
session,
|
|
24427
|
+
credentialType: "envVarValue",
|
|
24428
|
+
metadata: {
|
|
24429
|
+
key: pair.key,
|
|
24430
|
+
environment: pair.environment
|
|
24431
|
+
},
|
|
24432
|
+
secret: { value: pair.value }
|
|
24433
|
+
}), (envelope) => ({
|
|
24434
|
+
key: pair.key,
|
|
24435
|
+
environment: pair.environment,
|
|
24436
|
+
visibility: pair.visibility,
|
|
24437
|
+
value: {
|
|
24438
|
+
id: envelope.id,
|
|
24439
|
+
ciphertext: envelope.ciphertext,
|
|
24440
|
+
wrappedDek: envelope.wrappedDek,
|
|
24441
|
+
vaultVersion: envelope.vaultVersion
|
|
24442
|
+
}
|
|
24443
|
+
})), { concurrency: 8 });
|
|
24444
|
+
return yield* api["env-vars"].bulkImport({ payload: {
|
|
24445
|
+
scope: params.scope,
|
|
24446
|
+
...params.projectId === void 0 ? {} : { projectId: params.projectId },
|
|
24447
|
+
entries: sealed
|
|
24448
|
+
} });
|
|
24449
|
+
});
|
|
24329
24450
|
|
|
24330
24451
|
//#endregion
|
|
24331
24452
|
//#region src/lib/fingerprint.ts
|
|
@@ -28211,6 +28332,68 @@ const runCredentialsManager = Effect.gen(function* () {
|
|
|
28211
28332
|
yield* Console.log("Bye.");
|
|
28212
28333
|
});
|
|
28213
28334
|
|
|
28335
|
+
//#endregion
|
|
28336
|
+
//#region src/application/vault-rotation.ts
|
|
28337
|
+
/**
|
|
28338
|
+
* Re-key the org vault to `recipients`: generate a new vault key (v+1), unwrap
|
|
28339
|
+
* every credential + env-var DEK with the old key and re-wrap it under the new
|
|
28340
|
+
* one, re-wrap the new vault key to each recipient, then submit the rotation
|
|
28341
|
+
* atomically (the server CAS-guards on the current version and requires a
|
|
28342
|
+
* recovery recipient in the set). Drops every recipient not in `recipients`.
|
|
28343
|
+
*/
|
|
28344
|
+
const rotateVaultTo = (args) => Effect.gen(function* () {
|
|
28345
|
+
const orgId = yield* getActiveOrgId(args.api);
|
|
28346
|
+
const current = yield* unlockVaultKey(args.api, args.passphrase);
|
|
28347
|
+
const newVaultKey = generateVaultKey();
|
|
28348
|
+
const newVersion = current.vaultVersion + 1;
|
|
28349
|
+
const { deks } = yield* args.api.orgVault.listCredentialDeks();
|
|
28350
|
+
const credentialDeks = yield* Effect.forEach(deks, (dek) => Effect.try({
|
|
28351
|
+
try: () => {
|
|
28352
|
+
const raw = unwrapDek({
|
|
28353
|
+
wrappedDek: fromBase64(dek.wrappedDek),
|
|
28354
|
+
vaultKey: current.vaultKey,
|
|
28355
|
+
binding: {
|
|
28356
|
+
orgId,
|
|
28357
|
+
credentialId: dek.credentialId,
|
|
28358
|
+
vaultVersion: dek.vaultVersion
|
|
28359
|
+
}
|
|
28360
|
+
});
|
|
28361
|
+
return {
|
|
28362
|
+
credentialType: dek.credentialType,
|
|
28363
|
+
credentialId: dek.credentialId,
|
|
28364
|
+
wrappedDek: toBase64(wrapDek({
|
|
28365
|
+
dek: raw,
|
|
28366
|
+
vaultKey: newVaultKey,
|
|
28367
|
+
binding: {
|
|
28368
|
+
orgId,
|
|
28369
|
+
credentialId: dek.credentialId,
|
|
28370
|
+
vaultVersion: newVersion
|
|
28371
|
+
}
|
|
28372
|
+
}))
|
|
28373
|
+
};
|
|
28374
|
+
},
|
|
28375
|
+
catch: () => new IdentityError({ message: `Failed to re-wrap a ${dek.credentialType} DEK during rotation — re-unlock the vault and retry.` })
|
|
28376
|
+
}), { concurrency: "unbounded" });
|
|
28377
|
+
const recipientWraps = yield* Effect.forEach(args.recipients, (recipient) => Effect.promise(async () => ({
|
|
28378
|
+
userEncryptionKeyId: recipient.userEncryptionKeyId,
|
|
28379
|
+
wrappedKey: toBase64(await wrapVaultKey({
|
|
28380
|
+
vaultKey: newVaultKey,
|
|
28381
|
+
recipient: recipient.publicKey
|
|
28382
|
+
}))
|
|
28383
|
+
})), { concurrency: "unbounded" });
|
|
28384
|
+
return yield* args.api.orgVault.rotate({ payload: {
|
|
28385
|
+
fromVersion: current.vaultVersion,
|
|
28386
|
+
recipientWraps,
|
|
28387
|
+
credentialDeks
|
|
28388
|
+
} });
|
|
28389
|
+
});
|
|
28390
|
+
/** The encryption keys currently holding the vault key, joined with their public keys. */
|
|
28391
|
+
const currentRecipients = (api) => Effect.gen(function* () {
|
|
28392
|
+
const [wraps, keys] = yield* Effect.all([api.orgVault.listWraps(), api.userEncryptionKeys.list()]);
|
|
28393
|
+
const byId = new Map(keys.items.map((key) => [key.id, key]));
|
|
28394
|
+
return wraps.recipients.map((recipient) => byId.get(recipient.userEncryptionKeyId)).filter((key) => key !== void 0);
|
|
28395
|
+
});
|
|
28396
|
+
|
|
28214
28397
|
//#endregion
|
|
28215
28398
|
//#region src/commands/credentials/vault-session.ts
|
|
28216
28399
|
/**
|
|
@@ -28245,6 +28428,11 @@ const confirmFingerprint = (target, skip) => Effect.gen(function* () {
|
|
|
28245
28428
|
|
|
28246
28429
|
//#endregion
|
|
28247
28430
|
//#region src/commands/credentials/access.ts
|
|
28431
|
+
const RECOVERY_LABEL$1 = "Offline recovery key";
|
|
28432
|
+
const toRotationRecipient = (key) => ({
|
|
28433
|
+
userEncryptionKeyId: key.id,
|
|
28434
|
+
publicKey: key.publicKey
|
|
28435
|
+
});
|
|
28248
28436
|
const listCommand$5 = defineCommand({
|
|
28249
28437
|
meta: {
|
|
28250
28438
|
name: "list",
|
|
@@ -28299,14 +28487,153 @@ const grantCommand = defineCommand({
|
|
|
28299
28487
|
yield* printHuman(`Granted vault access to ${target.label} (${target.fingerprint}).`);
|
|
28300
28488
|
}))
|
|
28301
28489
|
});
|
|
28490
|
+
const confirmRecipients = (recipients, skip) => Effect.forEach(recipients, (recipient) => confirmFingerprint(recipient, skip), { discard: true });
|
|
28491
|
+
const rotateCommand = defineCommand({
|
|
28492
|
+
meta: {
|
|
28493
|
+
name: "rotate",
|
|
28494
|
+
description: "Rotate the vault key, re-wrapping every credential to the same recipients (admin)"
|
|
28495
|
+
},
|
|
28496
|
+
args: { yes: {
|
|
28497
|
+
type: "boolean",
|
|
28498
|
+
description: "Skip the out-of-band fingerprint confirmation prompt"
|
|
28499
|
+
} },
|
|
28500
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28501
|
+
const api = yield* apiClient;
|
|
28502
|
+
const recipients = yield* currentRecipients(api);
|
|
28503
|
+
yield* confirmRecipients(recipients, args.yes === true);
|
|
28504
|
+
const rotated = yield* rotateVaultTo({
|
|
28505
|
+
api,
|
|
28506
|
+
passphrase: yield* resolveVaultPassphrase,
|
|
28507
|
+
recipients: recipients.map(toRotationRecipient)
|
|
28508
|
+
});
|
|
28509
|
+
yield* printHuman(`Rotated the vault to version ${String(rotated.vaultVersion)} (${String(recipients.length)} recipients).`);
|
|
28510
|
+
}))
|
|
28511
|
+
});
|
|
28512
|
+
const revokeCommand$1 = defineCommand({
|
|
28513
|
+
meta: {
|
|
28514
|
+
name: "revoke",
|
|
28515
|
+
description: "Revoke a recipient and rotate the vault key so they can no longer decrypt (admin)"
|
|
28516
|
+
},
|
|
28517
|
+
args: {
|
|
28518
|
+
recipient: {
|
|
28519
|
+
type: "positional",
|
|
28520
|
+
required: false,
|
|
28521
|
+
description: "Key id or fingerprint of the recipient to revoke"
|
|
28522
|
+
},
|
|
28523
|
+
yes: {
|
|
28524
|
+
type: "boolean",
|
|
28525
|
+
description: "Skip the out-of-band fingerprint confirmation prompt"
|
|
28526
|
+
}
|
|
28527
|
+
},
|
|
28528
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28529
|
+
const api = yield* apiClient;
|
|
28530
|
+
const target = yield* findRecipient(api, yield* resolveSelector(args.recipient, "Recipient key id or fingerprint to revoke:"));
|
|
28531
|
+
const recipients = yield* currentRecipients(api);
|
|
28532
|
+
const surviving = recipients.filter((recipient) => recipient.id !== target.id);
|
|
28533
|
+
if (surviving.length === recipients.length) return yield* new IdentityError({ message: `${target.label} (${target.fingerprint}) is not a current vault recipient.` });
|
|
28534
|
+
if (!surviving.some((recipient) => recipient.kind === "recovery")) return yield* new IdentityError({ message: "Refusing to revoke the offline recovery recipient — rotate it with `credentials access recovery rotate` instead." });
|
|
28535
|
+
yield* confirmRecipients(surviving, args.yes === true);
|
|
28536
|
+
const rotated = yield* rotateVaultTo({
|
|
28537
|
+
api,
|
|
28538
|
+
passphrase: yield* resolveVaultPassphrase,
|
|
28539
|
+
recipients: surviving.map(toRotationRecipient)
|
|
28540
|
+
});
|
|
28541
|
+
yield* printHuman(`Revoked ${target.label} and rotated the vault to version ${String(rotated.vaultVersion)}.`);
|
|
28542
|
+
}))
|
|
28543
|
+
});
|
|
28544
|
+
const recoverCommand = defineCommand({
|
|
28545
|
+
meta: {
|
|
28546
|
+
name: "recover",
|
|
28547
|
+
description: "Restore this device's vault access with the offline recovery private key"
|
|
28548
|
+
},
|
|
28549
|
+
args: { key: {
|
|
28550
|
+
type: "string",
|
|
28551
|
+
description: "The offline recovery private key (AGE-SECRET-KEY-1...); prompted if omitted"
|
|
28552
|
+
} },
|
|
28553
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28554
|
+
const api = yield* apiClient;
|
|
28555
|
+
const recoveryPrivateKey = yield* resolveSelector(args.key, "Paste the offline recovery private key (AGE-SECRET-KEY-1...):");
|
|
28556
|
+
const { items } = yield* api.userEncryptionKeys.list();
|
|
28557
|
+
const recovery = items.find((key) => key.kind === "recovery" && key.revokedAt === null);
|
|
28558
|
+
if (!recovery) return yield* new IdentityError({ message: "This organization has no active recovery recipient to recover from." });
|
|
28559
|
+
const recipient = yield* activeRecipient;
|
|
28560
|
+
const own = items.find((key) => key.publicKey === recipient.publicKey);
|
|
28561
|
+
if (!own) return yield* new IdentityError({ message: "This device's encryption key is not registered. Run `better-update credentials identity register` first." });
|
|
28562
|
+
const wrap = yield* api.orgVault.getWrap({ path: { keyId: recovery.id } });
|
|
28563
|
+
const vaultKey = yield* Effect.tryPromise({
|
|
28564
|
+
try: async () => unwrapVaultKey({
|
|
28565
|
+
wrapped: fromBase64(wrap.wrappedKey),
|
|
28566
|
+
privateKey: recoveryPrivateKey
|
|
28567
|
+
}),
|
|
28568
|
+
catch: () => new IdentityError({ message: "Could not unwrap the vault key — the recovery private key is wrong." })
|
|
28569
|
+
});
|
|
28570
|
+
const wrapped = yield* Effect.promise(async () => wrapVaultKey({
|
|
28571
|
+
vaultKey,
|
|
28572
|
+
recipient: own.publicKey
|
|
28573
|
+
}));
|
|
28574
|
+
yield* api.orgVault.addWrap({ payload: {
|
|
28575
|
+
vaultVersion: wrap.vaultVersion,
|
|
28576
|
+
wrap: {
|
|
28577
|
+
userEncryptionKeyId: own.id,
|
|
28578
|
+
wrappedKey: toBase64(wrapped)
|
|
28579
|
+
}
|
|
28580
|
+
} });
|
|
28581
|
+
yield* printHuman(`Recovered vault access for this device (${own.label}).`);
|
|
28582
|
+
}))
|
|
28583
|
+
});
|
|
28584
|
+
const recoveryCommand = defineCommand({
|
|
28585
|
+
meta: {
|
|
28586
|
+
name: "recovery",
|
|
28587
|
+
description: "Manage the offline recovery recipient"
|
|
28588
|
+
},
|
|
28589
|
+
subCommands: { rotate: defineCommand({
|
|
28590
|
+
meta: {
|
|
28591
|
+
name: "rotate",
|
|
28592
|
+
description: "Mint a new offline recovery key and rotate the vault, revoking the old one (admin)"
|
|
28593
|
+
},
|
|
28594
|
+
args: { yes: {
|
|
28595
|
+
type: "boolean",
|
|
28596
|
+
description: "Skip the out-of-band fingerprint confirmation prompt"
|
|
28597
|
+
} },
|
|
28598
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28599
|
+
const api = yield* apiClient;
|
|
28600
|
+
const recipients = yield* currentRecipients(api);
|
|
28601
|
+
const newRecovery = yield* Effect.promise(async () => generateIdentity());
|
|
28602
|
+
const registered = yield* api.userEncryptionKeys.register({ payload: {
|
|
28603
|
+
kind: "recovery",
|
|
28604
|
+
publicKey: newRecovery.publicKey,
|
|
28605
|
+
label: RECOVERY_LABEL$1,
|
|
28606
|
+
fingerprint: newRecovery.fingerprint
|
|
28607
|
+
} });
|
|
28608
|
+
const surviving = recipients.filter((recipient) => recipient.kind !== "recovery");
|
|
28609
|
+
yield* confirmRecipients(surviving, args.yes === true);
|
|
28610
|
+
const rotated = yield* rotateVaultTo({
|
|
28611
|
+
api,
|
|
28612
|
+
passphrase: yield* resolveVaultPassphrase,
|
|
28613
|
+
recipients: [...surviving.map(toRotationRecipient), {
|
|
28614
|
+
userEncryptionKeyId: registered.id,
|
|
28615
|
+
publicKey: newRecovery.publicKey
|
|
28616
|
+
}]
|
|
28617
|
+
});
|
|
28618
|
+
yield* printKeyValue([["New recovery fingerprint", newRecovery.fingerprint], ["Vault version", String(rotated.vaultVersion)]]);
|
|
28619
|
+
yield* printHuman("Store this offline recovery private key safely — it is shown once and never again:");
|
|
28620
|
+
yield* Console.log(newRecovery.privateKey);
|
|
28621
|
+
}))
|
|
28622
|
+
}) },
|
|
28623
|
+
default: "rotate"
|
|
28624
|
+
});
|
|
28302
28625
|
const accessCommand = defineCommand({
|
|
28303
28626
|
meta: {
|
|
28304
28627
|
name: "access",
|
|
28305
|
-
description: "Inspect and
|
|
28628
|
+
description: "Inspect, grant, rotate, revoke, and recover access to the org credential vault"
|
|
28306
28629
|
},
|
|
28307
28630
|
subCommands: {
|
|
28308
28631
|
list: listCommand$5,
|
|
28309
|
-
grant: grantCommand
|
|
28632
|
+
grant: grantCommand,
|
|
28633
|
+
rotate: rotateCommand,
|
|
28634
|
+
revoke: revokeCommand$1,
|
|
28635
|
+
recover: recoverCommand,
|
|
28636
|
+
recovery: recoveryCommand
|
|
28310
28637
|
},
|
|
28311
28638
|
default: "list"
|
|
28312
28639
|
});
|
|
@@ -29242,7 +29569,8 @@ const createCommand$1 = defineCommand({
|
|
|
29242
29569
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
29243
29570
|
const label = yield* resolveLabel(args.label);
|
|
29244
29571
|
const identity = yield* createLocalIdentity(yield* promptNewPassphrase);
|
|
29245
|
-
|
|
29572
|
+
const api = yield* apiClient;
|
|
29573
|
+
yield* printRecipient(yield* registerRecipient(api, {
|
|
29246
29574
|
kind: "device",
|
|
29247
29575
|
publicKey: identity.publicKey,
|
|
29248
29576
|
fingerprint: identity.fingerprint,
|
|
@@ -29250,7 +29578,7 @@ const createCommand$1 = defineCommand({
|
|
|
29250
29578
|
}));
|
|
29251
29579
|
yield* printHuman("");
|
|
29252
29580
|
yield* printHuman("Sealed at ~/.better-update/identity.json — the private key never leaves this machine.");
|
|
29253
|
-
yield* printHuman(
|
|
29581
|
+
yield* printHuman(yield* orgVaultExists(api).pipe(Effect.map((exists) => exists ? VAULT_NOT_RECIPIENT_GUIDANCE : VAULT_NOT_SET_UP_GUIDANCE), Effect.orElseSucceed(() => VAULT_NOT_RECIPIENT_GUIDANCE)));
|
|
29254
29582
|
}))
|
|
29255
29583
|
});
|
|
29256
29584
|
const registerCommand = defineCommand({
|
|
@@ -30969,29 +31297,70 @@ const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
|
|
|
30969
31297
|
return raw;
|
|
30970
31298
|
});
|
|
30971
31299
|
const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
|
|
31300
|
+
const DOTENV_LINE = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/u;
|
|
31301
|
+
const stripQuotes = (raw) => {
|
|
31302
|
+
if (raw.length < 2) return raw;
|
|
31303
|
+
const [first] = raw;
|
|
31304
|
+
const last = raw.at(-1);
|
|
31305
|
+
return first === "\"" && last === "\"" || first === "'" && last === "'" ? raw.slice(1, -1) : raw;
|
|
31306
|
+
};
|
|
31307
|
+
const parseDotenvLine = (rawLine) => {
|
|
31308
|
+
const line = rawLine.trim();
|
|
31309
|
+
if (line === "" || line.startsWith("#")) return;
|
|
31310
|
+
const match = DOTENV_LINE.exec(line);
|
|
31311
|
+
if (!match) return;
|
|
31312
|
+
const [, key, rawValue] = match;
|
|
31313
|
+
if (key === void 0 || rawValue === void 0) return;
|
|
31314
|
+
return {
|
|
31315
|
+
key,
|
|
31316
|
+
value: stripQuotes(rawValue)
|
|
31317
|
+
};
|
|
31318
|
+
};
|
|
31319
|
+
/** Parse a dotenv file's `KEY=VALUE` lines (comments + blanks skipped, quotes stripped). */
|
|
31320
|
+
const parseDotenv = (content) => content.split(/\r?\n/u).map(parseDotenvLine).filter((entry) => entry !== void 0);
|
|
31321
|
+
/** Resolve a single project env var by (key, environment), or fail NotFound. */
|
|
31322
|
+
const findProjectEnvVar = (api, projectId, key, environment) => Effect.gen(function* () {
|
|
31323
|
+
const { items } = yield* api["env-vars"].list({ urlParams: {
|
|
31324
|
+
projectId,
|
|
31325
|
+
scope: "project",
|
|
31326
|
+
environments: environment
|
|
31327
|
+
} });
|
|
31328
|
+
const match = items.find((item) => item.key === key && item.environment === environment);
|
|
31329
|
+
if (!match) return yield* new EnvResourceNotFoundError({ message: `Env var "${key}" not found for environment "${environment}".` });
|
|
31330
|
+
return match;
|
|
31331
|
+
});
|
|
30972
31332
|
|
|
30973
31333
|
//#endregion
|
|
30974
31334
|
//#region src/commands/env/delete.ts
|
|
30975
31335
|
const deleteCommand$2 = defineCommand({
|
|
30976
31336
|
meta: {
|
|
30977
31337
|
name: "delete",
|
|
30978
|
-
description: "Delete a project env var by
|
|
31338
|
+
description: "Delete a project env var (one environment, or every environment by default)"
|
|
31339
|
+
},
|
|
31340
|
+
args: {
|
|
31341
|
+
key: {
|
|
31342
|
+
type: "positional",
|
|
31343
|
+
required: true,
|
|
31344
|
+
description: "Env var key"
|
|
31345
|
+
},
|
|
31346
|
+
environment: {
|
|
31347
|
+
type: "string",
|
|
31348
|
+
description: "Only delete this environment (default: every environment for the key)"
|
|
31349
|
+
}
|
|
30979
31350
|
},
|
|
30980
|
-
args: { key: {
|
|
30981
|
-
type: "positional",
|
|
30982
|
-
required: true,
|
|
30983
|
-
description: "Env var key"
|
|
30984
|
-
} },
|
|
30985
31351
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31352
|
+
const environment = args.environment === void 0 ? void 0 : yield* parseSingleEnvironmentArg(args.environment);
|
|
30986
31353
|
const projectId = yield* readProjectId;
|
|
30987
31354
|
const api = yield* apiClient;
|
|
30988
|
-
const
|
|
31355
|
+
const { items } = yield* api["env-vars"].list({ urlParams: {
|
|
30989
31356
|
projectId,
|
|
30990
|
-
scope: "project"
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
yield*
|
|
31357
|
+
scope: "project",
|
|
31358
|
+
...environment ? { environments: environment } : {}
|
|
31359
|
+
} });
|
|
31360
|
+
const matches = items.filter((item) => item.key === args.key && (environment === void 0 || item.environment === environment));
|
|
31361
|
+
if (matches.length === 0) return yield* new EnvResourceNotFoundError({ message: `Project env var "${args.key}" not found${environment ? ` for environment "${environment}"` : ""}.` });
|
|
31362
|
+
yield* Effect.forEach(matches, (match) => api["env-vars"].delete({ path: { id: match.id } }), { concurrency: 4 });
|
|
31363
|
+
yield* Console.log(`Deleted ${args.key} (${String(matches.length)} environment${matches.length === 1 ? "" : "s"})`);
|
|
30995
31364
|
}), envErrorExtras)
|
|
30996
31365
|
});
|
|
30997
31366
|
|
|
@@ -31021,10 +31390,10 @@ const getExecTrailingArgv = () => trailing$1;
|
|
|
31021
31390
|
|
|
31022
31391
|
//#endregion
|
|
31023
31392
|
//#region src/commands/env/exec.ts
|
|
31024
|
-
const pullForExec = (api, projectId, environment) => api
|
|
31393
|
+
const pullForExec = (api, projectId, environment) => pullEnvVars(api, {
|
|
31025
31394
|
projectId,
|
|
31026
31395
|
environment
|
|
31027
|
-
}
|
|
31396
|
+
}).pipe(Effect.catchAll(() => Effect.succeed({})));
|
|
31028
31397
|
const splitTrailing = (trailing) => {
|
|
31029
31398
|
if (!trailing || trailing.length === 0) return Effect.fail(new InvalidArgumentError({ message: "Pass the command after `--`. Example: `better-update env exec production -- bun run dev`." }));
|
|
31030
31399
|
const [bin, ...rest] = trailing;
|
|
@@ -31073,11 +31442,8 @@ const exportCommand = defineCommand({
|
|
|
31073
31442
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31074
31443
|
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31075
31444
|
const projectId = yield* readProjectId;
|
|
31076
|
-
const
|
|
31077
|
-
|
|
31078
|
-
environment
|
|
31079
|
-
} });
|
|
31080
|
-
for (const item of result.items) {
|
|
31445
|
+
const items = yield* exportDecryptedEnvVars(yield* apiClient, projectId, environment);
|
|
31446
|
+
for (const item of items) {
|
|
31081
31447
|
const escaped = item.value.replaceAll("'", String.raw`'\''`);
|
|
31082
31448
|
yield* Console.log(`${item.key}='${escaped}'`);
|
|
31083
31449
|
}
|
|
@@ -31086,43 +31452,21 @@ const exportCommand = defineCommand({
|
|
|
31086
31452
|
|
|
31087
31453
|
//#endregion
|
|
31088
31454
|
//#region src/commands/env/get.ts
|
|
31089
|
-
const resolveByKey = (api, key, environment) => Effect.gen(function* () {
|
|
31090
|
-
const env = environment === void 0 ? void 0 : yield* parseSingleEnvironmentArg(environment);
|
|
31091
|
-
const urlParams = {
|
|
31092
|
-
projectId: yield* readProjectId,
|
|
31093
|
-
scope: "all",
|
|
31094
|
-
search: key,
|
|
31095
|
-
...compact({ environments: env })
|
|
31096
|
-
};
|
|
31097
|
-
const { items } = yield* api["env-vars"].list({ urlParams });
|
|
31098
|
-
const matches = items.filter((item) => item.key === key);
|
|
31099
|
-
if (matches.length === 0) return yield* new EnvResourceNotFoundError({ message: `No env var with key "${key}" found${env === void 0 ? "" : ` for environment "${env}"`}.` });
|
|
31100
|
-
if (matches.length > 1) return yield* new EnvResourceNotFoundError({ message: `Multiple env vars match key "${key}". Disambiguate with --environment <${[...new Set(matches.flatMap((entry) => entry.environments))].join(", ")}>.` });
|
|
31101
|
-
return matches[0];
|
|
31102
|
-
});
|
|
31103
|
-
const renderValue$1 = (envVar, includeSensitive) => {
|
|
31104
|
-
if (envVar.visibility === "plaintext") return envVar.value ?? "";
|
|
31105
|
-
if (includeSensitive) return envVar.value ?? "";
|
|
31106
|
-
return "******";
|
|
31107
|
-
};
|
|
31108
31455
|
const getCommand$1 = defineCommand({
|
|
31109
31456
|
meta: {
|
|
31110
31457
|
name: "get",
|
|
31111
|
-
description: "Show an environment variable
|
|
31458
|
+
description: "Show an environment variable's effective value for an environment (decrypted locally)"
|
|
31112
31459
|
},
|
|
31113
31460
|
args: {
|
|
31114
31461
|
key: {
|
|
31115
31462
|
type: "positional",
|
|
31116
31463
|
required: true,
|
|
31117
|
-
description: "Env var KEY (uppercase)
|
|
31464
|
+
description: "Env var KEY (uppercase)"
|
|
31118
31465
|
},
|
|
31119
31466
|
environment: {
|
|
31120
31467
|
type: "string",
|
|
31121
|
-
|
|
31122
|
-
|
|
31123
|
-
"by-id": {
|
|
31124
|
-
type: "boolean",
|
|
31125
|
-
description: "Treat the argument as an ID instead of KEY"
|
|
31468
|
+
default: "production",
|
|
31469
|
+
description: "Target environment (development, preview, production)"
|
|
31126
31470
|
},
|
|
31127
31471
|
"include-sensitive": {
|
|
31128
31472
|
type: "boolean",
|
|
@@ -31130,21 +31474,62 @@ const getCommand$1 = defineCommand({
|
|
|
31130
31474
|
}
|
|
31131
31475
|
},
|
|
31132
31476
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31133
|
-
const
|
|
31134
|
-
const
|
|
31477
|
+
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31478
|
+
const projectId = yield* readProjectId;
|
|
31479
|
+
const match = (yield* exportDecryptedEnvVars(yield* apiClient, projectId, environment)).find((item) => item.key === args.key);
|
|
31480
|
+
if (!match) return yield* new EnvResourceNotFoundError({ message: `No env var "${args.key}" found for environment "${environment}".` });
|
|
31481
|
+
const includeSensitive = args["include-sensitive"] ?? false;
|
|
31482
|
+
const value = match.visibility === "sensitive" && !includeSensitive ? "******" : match.value;
|
|
31135
31483
|
yield* printKeyValue([
|
|
31136
|
-
["
|
|
31137
|
-
["
|
|
31138
|
-
["
|
|
31139
|
-
["
|
|
31140
|
-
["Visibility", envVar.visibility],
|
|
31141
|
-
["Value", renderValue$1(envVar, args["include-sensitive"] ?? false)],
|
|
31142
|
-
["Created", envVar.createdAt],
|
|
31143
|
-
["Updated", envVar.updatedAt]
|
|
31484
|
+
["Key", match.key],
|
|
31485
|
+
["Environment", environment],
|
|
31486
|
+
["Visibility", match.visibility],
|
|
31487
|
+
["Value", value]
|
|
31144
31488
|
]);
|
|
31145
31489
|
}), envErrorExtras)
|
|
31146
31490
|
});
|
|
31147
31491
|
|
|
31492
|
+
//#endregion
|
|
31493
|
+
//#region src/commands/env/history.ts
|
|
31494
|
+
const historyCommand = defineCommand({
|
|
31495
|
+
meta: {
|
|
31496
|
+
name: "history",
|
|
31497
|
+
description: "Show a project env var's value revision history (metadata only)"
|
|
31498
|
+
},
|
|
31499
|
+
args: {
|
|
31500
|
+
key: {
|
|
31501
|
+
type: "positional",
|
|
31502
|
+
required: true,
|
|
31503
|
+
description: "Env var key"
|
|
31504
|
+
},
|
|
31505
|
+
environment: {
|
|
31506
|
+
type: "string",
|
|
31507
|
+
default: "production",
|
|
31508
|
+
description: "Target environment (development, preview, production)"
|
|
31509
|
+
}
|
|
31510
|
+
},
|
|
31511
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31512
|
+
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31513
|
+
const projectId = yield* readProjectId;
|
|
31514
|
+
const api = yield* apiClient;
|
|
31515
|
+
const match = yield* findProjectEnvVar(api, projectId, args.key, environment);
|
|
31516
|
+
const { items } = yield* api["env-vars"].revisions({ path: { id: match.id } });
|
|
31517
|
+
yield* printList([
|
|
31518
|
+
"Revision",
|
|
31519
|
+
"Active",
|
|
31520
|
+
"Vault",
|
|
31521
|
+
"Created",
|
|
31522
|
+
"By"
|
|
31523
|
+
], items.map((revision) => [
|
|
31524
|
+
String(revision.revisionNumber),
|
|
31525
|
+
revision.isCurrent ? "current" : "",
|
|
31526
|
+
String(revision.vaultVersion),
|
|
31527
|
+
revision.createdAt,
|
|
31528
|
+
revision.createdBy ?? "-"
|
|
31529
|
+
]), "No revisions found.");
|
|
31530
|
+
}), envErrorExtras)
|
|
31531
|
+
});
|
|
31532
|
+
|
|
31148
31533
|
//#endregion
|
|
31149
31534
|
//#region src/commands/env/import.ts
|
|
31150
31535
|
const importCommand = defineCommand({
|
|
@@ -31171,31 +31556,33 @@ const importCommand = defineCommand({
|
|
|
31171
31556
|
}
|
|
31172
31557
|
},
|
|
31173
31558
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31174
|
-
const
|
|
31559
|
+
const entries = parseDotenv(yield* (yield* FileSystem.FileSystem).readFileString(args.file)).map((entry) => ({
|
|
31560
|
+
key: entry.key,
|
|
31561
|
+
value: entry.value,
|
|
31562
|
+
visibility: args.visibility
|
|
31563
|
+
}));
|
|
31564
|
+
if (entries.length === 0) {
|
|
31565
|
+
yield* Console.log(`No valid KEY=VALUE entries found in ${args.file}.`);
|
|
31566
|
+
return;
|
|
31567
|
+
}
|
|
31175
31568
|
const environments = yield* parseEnvironmentsArg(args.environment);
|
|
31176
31569
|
const projectId = yield* readProjectId;
|
|
31177
|
-
const result = yield* (yield* apiClient
|
|
31570
|
+
const result = yield* uploadEnvVars(yield* apiClient, {
|
|
31178
31571
|
scope: "project",
|
|
31179
31572
|
projectId,
|
|
31180
31573
|
environments,
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
} });
|
|
31574
|
+
entries
|
|
31575
|
+
});
|
|
31184
31576
|
yield* Console.log(`Imported: ${String(result.created)} created, ${String(result.updated)} updated, ${String(result.skipped)} skipped`);
|
|
31185
31577
|
}), envErrorExtras)
|
|
31186
31578
|
});
|
|
31187
31579
|
|
|
31188
31580
|
//#endregion
|
|
31189
31581
|
//#region src/commands/env/list.ts
|
|
31190
|
-
const renderValue = (item, includeSensitive) => {
|
|
31191
|
-
if (item.visibility === "plaintext") return item.value ?? "";
|
|
31192
|
-
if (includeSensitive) return item.value ?? "";
|
|
31193
|
-
return "••••••";
|
|
31194
|
-
};
|
|
31195
31582
|
const listCommand$2 = defineCommand({
|
|
31196
31583
|
meta: {
|
|
31197
31584
|
name: "list",
|
|
31198
|
-
description: "List environment
|
|
31585
|
+
description: "List environment variable metadata. Values are end-to-end encrypted — read them with `env pull`, `env export`, or `env get`."
|
|
31199
31586
|
},
|
|
31200
31587
|
args: {
|
|
31201
31588
|
environments: {
|
|
@@ -31214,10 +31601,6 @@ const listCommand$2 = defineCommand({
|
|
|
31214
31601
|
search: {
|
|
31215
31602
|
type: "string",
|
|
31216
31603
|
description: "Filter by key substring (case-insensitive)"
|
|
31217
|
-
},
|
|
31218
|
-
"include-sensitive": {
|
|
31219
|
-
type: "boolean",
|
|
31220
|
-
description: "Reveal masked sensitive values (default: masked)"
|
|
31221
31604
|
}
|
|
31222
31605
|
},
|
|
31223
31606
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
@@ -31229,20 +31612,18 @@ const listCommand$2 = defineCommand({
|
|
|
31229
31612
|
...args.environments ? { environments: args.environments } : {},
|
|
31230
31613
|
...args.search ? { search: args.search } : {}
|
|
31231
31614
|
};
|
|
31232
|
-
const result = yield* api["env-vars"].list({ urlParams });
|
|
31233
|
-
const includeSensitive = args["include-sensitive"] ?? false;
|
|
31234
31615
|
yield* printList([
|
|
31235
31616
|
"Key",
|
|
31236
|
-
"
|
|
31617
|
+
"Environment",
|
|
31237
31618
|
"Scope",
|
|
31238
31619
|
"Visibility",
|
|
31239
|
-
"
|
|
31240
|
-
],
|
|
31620
|
+
"Revisions"
|
|
31621
|
+
], (yield* api["env-vars"].list({ urlParams })).items.map((item) => [
|
|
31241
31622
|
item.key,
|
|
31242
|
-
|
|
31623
|
+
item.environment,
|
|
31243
31624
|
item.overridesGlobal ? `${item.scope} (overrides global)` : item.scope,
|
|
31244
31625
|
item.visibility,
|
|
31245
|
-
|
|
31626
|
+
String(item.revisionCount)
|
|
31246
31627
|
]), "No environment variables found.");
|
|
31247
31628
|
}), envErrorExtras)
|
|
31248
31629
|
});
|
|
@@ -31293,18 +31674,15 @@ const pullCommand = defineCommand({
|
|
|
31293
31674
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31294
31675
|
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31295
31676
|
const projectId = yield* readProjectId;
|
|
31296
|
-
const
|
|
31297
|
-
projectId,
|
|
31298
|
-
environment
|
|
31299
|
-
} });
|
|
31677
|
+
const items = yield* exportDecryptedEnvVars(yield* apiClient, projectId, environment);
|
|
31300
31678
|
if (args.stdout) {
|
|
31301
|
-
yield* printStdout(
|
|
31679
|
+
yield* printStdout(items);
|
|
31302
31680
|
return;
|
|
31303
31681
|
}
|
|
31304
31682
|
const cwd = yield* (yield* CliRuntime).cwd;
|
|
31305
31683
|
yield* writeDotenvFile({
|
|
31306
31684
|
targetPath: path.resolve(cwd, args.path ?? DEFAULT_PATH),
|
|
31307
|
-
items
|
|
31685
|
+
items,
|
|
31308
31686
|
force: args.force ?? false
|
|
31309
31687
|
});
|
|
31310
31688
|
}), envErrorExtras)
|
|
@@ -31312,32 +31690,11 @@ const pullCommand = defineCommand({
|
|
|
31312
31690
|
|
|
31313
31691
|
//#endregion
|
|
31314
31692
|
//#region src/commands/env/push.ts
|
|
31315
|
-
const LINE_PATTERN = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/u;
|
|
31316
|
-
const stripQuotes = (raw) => {
|
|
31317
|
-
if (raw.length < 2) return raw;
|
|
31318
|
-
const [first] = raw;
|
|
31319
|
-
const last = raw.at(-1);
|
|
31320
|
-
return first === "\"" && last === "\"" || first === "'" && last === "'" ? raw.slice(1, -1) : raw;
|
|
31321
|
-
};
|
|
31322
31693
|
const classifyVisibility = (key) => key.startsWith("EXPO_PUBLIC_") ? "plaintext" : "sensitive";
|
|
31323
|
-
const parseLine = (rawLine) => {
|
|
31324
|
-
const line = rawLine.trim();
|
|
31325
|
-
if (line === "" || line.startsWith("#")) return;
|
|
31326
|
-
const match = LINE_PATTERN.exec(line);
|
|
31327
|
-
if (!match) return;
|
|
31328
|
-
const [, key, rawValue] = match;
|
|
31329
|
-
if (key === void 0 || rawValue === void 0) return;
|
|
31330
|
-
return {
|
|
31331
|
-
key,
|
|
31332
|
-
value: stripQuotes(rawValue),
|
|
31333
|
-
visibility: classifyVisibility(key)
|
|
31334
|
-
};
|
|
31335
|
-
};
|
|
31336
|
-
const parseDotenv = (content) => content.split(/\r?\n/u).map(parseLine).filter((entry) => entry !== void 0);
|
|
31337
31694
|
const pushCommand = defineCommand({
|
|
31338
31695
|
meta: {
|
|
31339
31696
|
name: "push",
|
|
31340
|
-
description: "Push env vars from a dotenv file. Auto-classifies EXPO_PUBLIC_* as plaintext, others as sensitive."
|
|
31697
|
+
description: "Push (encrypt + upsert) env vars from a dotenv file. Auto-classifies EXPO_PUBLIC_* as plaintext, others as sensitive."
|
|
31341
31698
|
},
|
|
31342
31699
|
args: {
|
|
31343
31700
|
file: {
|
|
@@ -31350,10 +31707,6 @@ const pushCommand = defineCommand({
|
|
|
31350
31707
|
type: "string",
|
|
31351
31708
|
default: "production",
|
|
31352
31709
|
description: "Target environments (comma-separated, e.g. development,production). Default: production"
|
|
31353
|
-
},
|
|
31354
|
-
force: {
|
|
31355
|
-
type: "boolean",
|
|
31356
|
-
description: "Overwrite existing vars without prompting"
|
|
31357
31710
|
}
|
|
31358
31711
|
},
|
|
31359
31712
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
@@ -31364,49 +31717,57 @@ const pushCommand = defineCommand({
|
|
|
31364
31717
|
}
|
|
31365
31718
|
const environments = yield* parseEnvironmentsArg(args.environment);
|
|
31366
31719
|
const projectId = yield* readProjectId;
|
|
31367
|
-
const
|
|
31368
|
-
const existingResp = yield* api["env-vars"].list({ urlParams: {
|
|
31369
|
-
projectId,
|
|
31370
|
-
scope: "project"
|
|
31371
|
-
} });
|
|
31372
|
-
const existingByKey = new Map(existingResp.items.map((item) => [item.key, item]));
|
|
31373
|
-
const conflicts = parsed.filter((entry) => existingByKey.has(entry.key));
|
|
31374
|
-
const newEntries = parsed.filter((entry) => !existingByKey.has(entry.key));
|
|
31375
|
-
const entriesToOverwrite = yield* Effect.gen(function* () {
|
|
31376
|
-
if (conflicts.length === 0 || args.force) return conflicts;
|
|
31377
|
-
if (!(yield* InteractiveMode).allow) {
|
|
31378
|
-
const conflictKeys = conflicts.map((conflict) => conflict.key).join(", ");
|
|
31379
|
-
return yield* new InvalidArgumentError({ message: `${String(conflicts.length)} conflict(s): ${conflictKeys}. Pass --force to overwrite or run interactively.` });
|
|
31380
|
-
}
|
|
31381
|
-
const picked = yield* promptMultiSelect("Overwrite which existing vars?", conflicts.map((entry) => ({
|
|
31382
|
-
value: entry.key,
|
|
31383
|
-
label: `${entry.key} (${existingByKey.get(entry.key)?.visibility ?? "?"} → ${entry.visibility})`
|
|
31384
|
-
})));
|
|
31385
|
-
const pickedSet = new Set(picked);
|
|
31386
|
-
return conflicts.filter((entry) => pickedSet.has(entry.key));
|
|
31387
|
-
});
|
|
31388
|
-
const skipped = conflicts.length - entriesToOverwrite.length;
|
|
31389
|
-
yield* Effect.forEach(newEntries, (entry) => api["env-vars"].create({ payload: {
|
|
31720
|
+
const result = yield* uploadEnvVars(yield* apiClient, {
|
|
31390
31721
|
scope: "project",
|
|
31391
31722
|
projectId,
|
|
31392
31723
|
environments,
|
|
31393
|
-
|
|
31394
|
-
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
|
|
31399
|
-
|
|
31400
|
-
|
|
31401
|
-
|
|
31402
|
-
|
|
31403
|
-
|
|
31404
|
-
|
|
31405
|
-
|
|
31406
|
-
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31724
|
+
entries: parsed.map((entry) => ({
|
|
31725
|
+
key: entry.key,
|
|
31726
|
+
value: entry.value,
|
|
31727
|
+
visibility: classifyVisibility(entry.key)
|
|
31728
|
+
}))
|
|
31729
|
+
});
|
|
31730
|
+
yield* printHuman(`Pushed to ${formatEnvironments(environments)}: ${String(result.created)} created, ${String(result.updated)} updated${result.skipped > 0 ? `, ${String(result.skipped)} skipped` : ""}.`);
|
|
31731
|
+
}), envErrorExtras)
|
|
31732
|
+
});
|
|
31733
|
+
|
|
31734
|
+
//#endregion
|
|
31735
|
+
//#region src/commands/env/rollback.ts
|
|
31736
|
+
const rollbackCommand$1 = defineCommand({
|
|
31737
|
+
meta: {
|
|
31738
|
+
name: "rollback",
|
|
31739
|
+
description: "Roll a project env var back to an earlier value revision"
|
|
31740
|
+
},
|
|
31741
|
+
args: {
|
|
31742
|
+
key: {
|
|
31743
|
+
type: "positional",
|
|
31744
|
+
required: true,
|
|
31745
|
+
description: "Env var key"
|
|
31746
|
+
},
|
|
31747
|
+
to: {
|
|
31748
|
+
type: "string",
|
|
31749
|
+
required: true,
|
|
31750
|
+
description: "Target revision number (from `env history`) or revision id"
|
|
31751
|
+
},
|
|
31752
|
+
environment: {
|
|
31753
|
+
type: "string",
|
|
31754
|
+
default: "production",
|
|
31755
|
+
description: "Target environment (development, preview, production)"
|
|
31756
|
+
}
|
|
31757
|
+
},
|
|
31758
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31759
|
+
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31760
|
+
const projectId = yield* readProjectId;
|
|
31761
|
+
const api = yield* apiClient;
|
|
31762
|
+
const match = yield* findProjectEnvVar(api, projectId, args.key, environment);
|
|
31763
|
+
const { items } = yield* api["env-vars"].revisions({ path: { id: match.id } });
|
|
31764
|
+
const target = items.find((revision) => revision.id === args.to || String(revision.revisionNumber) === args.to);
|
|
31765
|
+
if (!target) return yield* new InvalidArgumentError({ message: `Revision "${args.to}" not found for ${args.key} (${environment}). See \`env history\`.` });
|
|
31766
|
+
yield* api["env-vars"].rollback({
|
|
31767
|
+
path: { id: match.id },
|
|
31768
|
+
payload: { toRevisionId: target.id }
|
|
31769
|
+
});
|
|
31770
|
+
yield* Console.log(`Rolled back ${args.key} (${environment}) to revision ${String(target.revisionNumber)}.`);
|
|
31410
31771
|
}), envErrorExtras)
|
|
31411
31772
|
});
|
|
31412
31773
|
|
|
@@ -31432,7 +31793,7 @@ const setCommand$1 = defineCommand({
|
|
|
31432
31793
|
type: "enum",
|
|
31433
31794
|
options: ["plaintext", "sensitive"],
|
|
31434
31795
|
default: "plaintext",
|
|
31435
|
-
description: "Value visibility"
|
|
31796
|
+
description: "Value visibility (build-log redaction hint)"
|
|
31436
31797
|
}
|
|
31437
31798
|
},
|
|
31438
31799
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
@@ -31440,33 +31801,18 @@ const setCommand$1 = defineCommand({
|
|
|
31440
31801
|
const environments = yield* parseEnvironmentsArg(args.environment);
|
|
31441
31802
|
const { visibility } = args;
|
|
31442
31803
|
const projectId = yield* readProjectId;
|
|
31443
|
-
const
|
|
31444
|
-
|
|
31804
|
+
const result = yield* uploadEnvVars(yield* apiClient, {
|
|
31805
|
+
scope: "project",
|
|
31445
31806
|
projectId,
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
const label = formatEnvironments(environments);
|
|
31449
|
-
if (match) {
|
|
31450
|
-
yield* api["env-vars"].update({
|
|
31451
|
-
path: { id: match.id },
|
|
31452
|
-
payload: {
|
|
31453
|
-
value,
|
|
31454
|
-
visibility,
|
|
31455
|
-
environments
|
|
31456
|
-
}
|
|
31457
|
-
});
|
|
31458
|
-
yield* Console.log(`Updated ${key} (environments: ${label})`);
|
|
31459
|
-
} else {
|
|
31460
|
-
yield* api["env-vars"].create({ payload: {
|
|
31461
|
-
scope: "project",
|
|
31462
|
-
projectId,
|
|
31463
|
-
environments,
|
|
31807
|
+
environments,
|
|
31808
|
+
entries: [{
|
|
31464
31809
|
key,
|
|
31465
31810
|
value,
|
|
31466
31811
|
visibility
|
|
31467
|
-
}
|
|
31468
|
-
|
|
31469
|
-
|
|
31812
|
+
}]
|
|
31813
|
+
});
|
|
31814
|
+
const label = formatEnvironments(environments);
|
|
31815
|
+
yield* Console.log(`Set ${key} (environments: ${label}; ${result.created} created, ${result.updated} updated)`);
|
|
31470
31816
|
}), envErrorExtras)
|
|
31471
31817
|
});
|
|
31472
31818
|
|
|
@@ -31475,7 +31821,7 @@ const setCommand$1 = defineCommand({
|
|
|
31475
31821
|
const updateCommand$1 = defineCommand({
|
|
31476
31822
|
meta: {
|
|
31477
31823
|
name: "update",
|
|
31478
|
-
description: "Update a project env var's value
|
|
31824
|
+
description: "Update a project env var's value or visibility for an environment"
|
|
31479
31825
|
},
|
|
31480
31826
|
args: {
|
|
31481
31827
|
key: {
|
|
@@ -31483,6 +31829,11 @@ const updateCommand$1 = defineCommand({
|
|
|
31483
31829
|
required: true,
|
|
31484
31830
|
description: "Env var key (e.g. API_KEY)"
|
|
31485
31831
|
},
|
|
31832
|
+
environment: {
|
|
31833
|
+
type: "string",
|
|
31834
|
+
default: "production",
|
|
31835
|
+
description: "Target environment (development, preview, production)"
|
|
31836
|
+
},
|
|
31486
31837
|
value: {
|
|
31487
31838
|
type: "string",
|
|
31488
31839
|
description: "New value (leave unset to keep current)"
|
|
@@ -31491,37 +31842,52 @@ const updateCommand$1 = defineCommand({
|
|
|
31491
31842
|
type: "enum",
|
|
31492
31843
|
options: ["plaintext", "sensitive"],
|
|
31493
31844
|
description: "New visibility (leave unset to keep current)"
|
|
31494
|
-
},
|
|
31495
|
-
environments: {
|
|
31496
|
-
type: "string",
|
|
31497
|
-
description: "New environments assignment (comma-separated, e.g. development,production). Leave unset to keep current."
|
|
31498
31845
|
}
|
|
31499
31846
|
},
|
|
31500
31847
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31501
|
-
const { key, value, visibility
|
|
31502
|
-
if (value === void 0 && visibility === void 0
|
|
31848
|
+
const { key, value, visibility } = args;
|
|
31849
|
+
if (value === void 0 && visibility === void 0) return yield* new InvalidArgumentError({ message: "Pass --value and/or --visibility. Nothing to update otherwise." });
|
|
31850
|
+
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
31503
31851
|
const projectId = yield* readProjectId;
|
|
31504
31852
|
const api = yield* apiClient;
|
|
31505
|
-
const
|
|
31853
|
+
const { items } = yield* api["env-vars"].list({ urlParams: {
|
|
31506
31854
|
projectId,
|
|
31507
|
-
scope: "project"
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
const
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
visibility,
|
|
31514
|
-
environments: envList
|
|
31515
|
-
});
|
|
31516
|
-
yield* api["env-vars"].update({
|
|
31855
|
+
scope: "project",
|
|
31856
|
+
environments: environment
|
|
31857
|
+
} });
|
|
31858
|
+
const match = items.find((item) => item.key === key && item.environment === environment);
|
|
31859
|
+
if (!match) return yield* new EnvResourceNotFoundError({ message: `Env var "${key}" not found for environment "${environment}".` });
|
|
31860
|
+
if (value === void 0) yield* api["env-vars"].update({
|
|
31517
31861
|
path: { id: match.id },
|
|
31518
|
-
payload
|
|
31862
|
+
payload: compact({ visibility })
|
|
31519
31863
|
});
|
|
31864
|
+
else {
|
|
31865
|
+
const envelope = yield* sealForUpload({
|
|
31866
|
+
session: yield* openVaultSessionInteractive(api),
|
|
31867
|
+
credentialType: "envVarValue",
|
|
31868
|
+
metadata: {
|
|
31869
|
+
key,
|
|
31870
|
+
environment
|
|
31871
|
+
},
|
|
31872
|
+
secret: { value }
|
|
31873
|
+
});
|
|
31874
|
+
yield* api["env-vars"].update({
|
|
31875
|
+
path: { id: match.id },
|
|
31876
|
+
payload: {
|
|
31877
|
+
value: {
|
|
31878
|
+
id: envelope.id,
|
|
31879
|
+
ciphertext: envelope.ciphertext,
|
|
31880
|
+
wrappedDek: envelope.wrappedDek,
|
|
31881
|
+
vaultVersion: envelope.vaultVersion
|
|
31882
|
+
},
|
|
31883
|
+
...compact({ visibility })
|
|
31884
|
+
}
|
|
31885
|
+
});
|
|
31886
|
+
}
|
|
31520
31887
|
const changed = [];
|
|
31521
31888
|
if (value !== void 0) changed.push("value");
|
|
31522
31889
|
if (visibility !== void 0) changed.push("visibility");
|
|
31523
|
-
|
|
31524
|
-
yield* printHuman(`Updated ${changed.join(" + ")} for ${key}.`);
|
|
31890
|
+
yield* printHuman(`Updated ${changed.join(" + ")} for ${key} (${environment}).`);
|
|
31525
31891
|
}), envErrorExtras)
|
|
31526
31892
|
});
|
|
31527
31893
|
|
|
@@ -31538,6 +31904,8 @@ const envCommand = defineCommand({
|
|
|
31538
31904
|
set: setCommand$1,
|
|
31539
31905
|
update: updateCommand$1,
|
|
31540
31906
|
delete: deleteCommand$2,
|
|
31907
|
+
history: historyCommand,
|
|
31908
|
+
rollback: rollbackCommand$1,
|
|
31541
31909
|
import: importCommand,
|
|
31542
31910
|
push: pushCommand,
|
|
31543
31911
|
export: exportCommand,
|