@seekrit/cli 0.47.0 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1118 -420
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from "node:child_process";
3
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  import { Command } from "commander";
6
6
  import { arch, homedir, hostname, platform, tmpdir, userInfo } from "node:os";
@@ -8,6 +8,7 @@ import { dirname, join, parse, resolve } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
9
  import { Writable } from "node:stream";
10
10
  import { createHash } from "node:crypto";
11
+ import { fileURLToPath } from "node:url";
11
12
  /** Default lifetime of a published bundle (7 days), in seconds. */
12
13
  const POLICY_DEFAULT_TTL_SECONDS = 10080 * 60;
13
14
  /** Bounds on a bundle's lifetime: an hour at the short end, 90 days at the long. */
@@ -4986,7 +4987,7 @@ async function createAgentTaskToken() {
4986
4987
  }
4987
4988
  //#endregion
4988
4989
  //#region package.json
4989
- var version = "0.47.0";
4990
+ var version = "1.1.0";
4990
4991
  //#endregion
4991
4992
  //#region ../../packages/api-client/src/index.ts
4992
4993
  var SeekritApiError = class extends Error {
@@ -5765,7 +5766,18 @@ function parseDurationSeconds(input, flag) {
5765
5766
  d: 86400
5766
5767
  }[m[2] || "s"] ?? 1);
5767
5768
  }
5768
- /** Prompt without echoing input (for passphrases). */
5769
+ /**
5770
+ * Prompt without echoing input (for passphrases).
5771
+ *
5772
+ * Piping the answer in (`echo … | seekrit secrets get …`) is supported and
5773
+ * common in CI, so this reads stdin rather than insisting on a TTY. But stdin
5774
+ * can also close with nothing on it — `< /dev/null`, a closed pipe, an agent
5775
+ * spawning us with no stdin — and readline signals that by emitting `close`
5776
+ * without ever calling the `question` callback. Left unhandled the promise
5777
+ * never settles, the event loop drains, and Node exits **0** having printed
5778
+ * nothing: `V=$(seekrit secrets get X)` silently yields an empty value and a
5779
+ * success status. So treat EOF-without-an-answer as the error it is.
5780
+ */
5769
5781
  function promptHidden(question) {
5770
5782
  const muted = new Writable({ write(_chunk, _encoding, callback) {
5771
5783
  callback();
@@ -5776,8 +5788,15 @@ function promptHidden(question) {
5776
5788
  output: muted,
5777
5789
  terminal: true
5778
5790
  });
5779
- return new Promise((resolve) => {
5791
+ return new Promise((resolve, reject) => {
5792
+ let answered = false;
5793
+ rl.on("close", () => {
5794
+ if (answered) return;
5795
+ process.stderr.write("\n");
5796
+ reject(/* @__PURE__ */ new Error("no passphrase on stdin — set SEEKRIT_PASSPHRASE, pipe it in, or run this in a terminal"));
5797
+ });
5780
5798
  rl.question("", (answer) => {
5799
+ answered = true;
5781
5800
  rl.close();
5782
5801
  process.stderr.write("\n");
5783
5802
  resolve(answer);
@@ -5853,9 +5872,23 @@ const CLI_CLIENT = `cli/${version}`;
5853
5872
  * `flag > env > .env` credential resolution. Empty for every command but
5854
5873
  * `seekrit run`, which loads `.env` before authenticating.
5855
5874
  */
5875
+ /**
5876
+ * A `SEEKRIT_*` value, or undefined if it is absent *or blank*.
5877
+ *
5878
+ * Blank has to mean absent. An unset CI secret, a `${VAR}` that expanded to
5879
+ * nothing, a bare `export SEEKRIT_TOKEN=` — all arrive as `""`, and `??` only
5880
+ * falls back on null/undefined. Left alone, an empty `SEEKRIT_API_URL` makes
5881
+ * every request relative and an empty `SEEKRIT_TOKEN` authenticates as a
5882
+ * bearer of nothing: a 401 where the honest answer is "you have no
5883
+ * credentials", pointing at the API instead of at the missing variable.
5884
+ */
5885
+ function present(value) {
5886
+ const trimmed = value?.trim();
5887
+ return trimmed ? trimmed : void 0;
5888
+ }
5856
5889
  function tryBuildContext(dotenvVars = {}) {
5857
5890
  const config = readGlobalConfig();
5858
- const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
5891
+ const fromEnv = (key) => present(process.env[key]) ?? present(dotenvVars[key]);
5859
5892
  const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
5860
5893
  const token = fromEnv("SEEKRIT_TOKEN") ?? config.token ?? config.sessionToken;
5861
5894
  const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
@@ -5888,6 +5921,24 @@ function isTokenAuth(ctx) {
5888
5921
  return ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token);
5889
5922
  }
5890
5923
  /**
5924
+ * Render an unhandled error for the top-level handler.
5925
+ *
5926
+ * `fetch` reports every transport failure as the same opaque `TypeError: fetch
5927
+ * failed` and hides the reason (ECONNREFUSED, DNS, TLS) in `cause`. That is the
5928
+ * single most common thing to go wrong — a stale `SEEKRIT_API_URL`, a dev
5929
+ * server that isn't up, a VPN that is down — and "error: fetch failed" names
5930
+ * neither the address we tried nor why it failed. So unwrap it and say both.
5931
+ */
5932
+ function describeError(err) {
5933
+ if (!(err instanceof Error)) return String(err);
5934
+ if (err.message !== "fetch failed") return err.message;
5935
+ const config = readGlobalConfig();
5936
+ const apiUrl = present(process.env.SEEKRIT_API_URL) ?? config.apiUrl ?? "https://api.seekrit.dev";
5937
+ const cause = err.cause;
5938
+ const reason = cause instanceof Error ? cause.code ?? cause.message : void 0;
5939
+ return `cannot reach the seekrit API at ${apiUrl}${reason ? ` (${reason})` : ""} — check SEEKRIT_API_URL, your network, and that the API is up`;
5940
+ }
5941
+ /**
5891
5942
  * Recover the calling principal's private key:
5892
5943
  * - service tokens carry their private key in the token string;
5893
5944
  * - users fetch their passphrase-encrypted key from the API and unlock it.
@@ -6150,7 +6201,7 @@ async function resolvePrincipal(ctx, orgId, options) {
6150
6201
  }
6151
6202
  /** The `--org/--app/--group/--env` selection every grant command shares. */
6152
6203
  function withEnvTarget(cmd) {
6153
- return cmd.option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").requiredOption("--env <slug>");
6204
+ return cmd.option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").requiredOption("--env <slug>", "environment slug");
6154
6205
  }
6155
6206
  /**
6156
6207
  * Environment key grants — who can decrypt what.
@@ -6185,7 +6236,7 @@ function registerAccessCommands(program) {
6185
6236
  col("id", (g) => g.principalId)
6186
6237
  ], "nobody holds a key for this environment"));
6187
6238
  });
6188
- withEnvTarget(grant.command("rm").alias("revoke").description("take away a member's or token's access to an environment's key").option("--user <email>", "revoke an org member by email").option("--token <tokenId>", "revoke a service token by id (skt_…)").option("--yes", "skip the confirmation prompt")).action(async (options) => {
6239
+ withEnvTarget(grant.command("rm").alias("revoke").description("take away a member's or token's access to an environment's key").option("--user <email>", "revoke an org member by email").option("--token <tokenId>", "revoke a service token by id (skt_…)").option("-y, --yes", "skip the confirmation prompt")).action(async (options) => {
6189
6240
  if (!options.user === !options.token) fail("pass exactly one of --user or --token");
6190
6241
  const ctx = buildContext();
6191
6242
  const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
@@ -6231,7 +6282,7 @@ function registerAccountCommands(program) {
6231
6282
  col("id", (s) => `${s.id}${s.id === currentSessionId ? " (this one)" : ""}`)
6232
6283
  ], options.all ? "no CLI sessions" : "no active CLI sessions (try --all)"));
6233
6284
  });
6234
- session.command("revoke <sessionId>").description("sign a device out — its token stops working immediately").option("--yes", "skip the confirmation prompt").action(async (sessionId, options) => {
6285
+ session.command("revoke <sessionId>").description("sign a device out — its token stops working immediately").option("-y, --yes", "skip the confirmation prompt").action(async (sessionId, options) => {
6235
6286
  const ctx = buildContext();
6236
6287
  const self = ctx.auth.type === "bearer" && isCliSessionToken(ctx.auth.token) && parseCliSessionToken(ctx.auth.token).sessionId === sessionId;
6237
6288
  await confirmDestructive(options.yes, self ? `${sessionId} is the session you are using right now — sign it out?` : `Sign out ${sessionId}?`);
@@ -6962,7 +7013,7 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
6962
7013
  }
6963
7014
  function registerKmsCommands(program) {
6964
7015
  const kms = program.command("kms").description("managed keys for application-layer encryption & signing (client-side)");
6965
- kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$7, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$7, []).action(async (options) => {
7016
+ kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$7, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$7, []).action(async (options) => {
6966
7017
  if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
6967
7018
  if (options.app && options.group) fail("pass at most one of --app or --group");
6968
7019
  const ctx = buildContext();
@@ -7015,7 +7066,7 @@ function registerKmsCommands(program) {
7015
7066
  const { key } = await ctx.client.createKmsKey(org.id, input);
7016
7067
  console.error(`created ${key.purpose} key ${key.name} (${key.id}), ${grants.length} grant(s)`);
7017
7068
  });
7018
- kms.command("ls").description("list keys you can see").option("--org <slug>").action(async (options) => {
7069
+ kms.command("ls").description("list keys you can see").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7019
7070
  const ctx = buildContext();
7020
7071
  const org = await resolveOrg(ctx, options.org);
7021
7072
  const { keys } = await ctx.client.listKmsKeys(org.id);
@@ -7029,7 +7080,7 @@ function registerKmsCommands(program) {
7029
7080
  console.log(`${k.name}\t${k.purpose}\tv${k.currentVersion}\t${scope}\t${k.id}${state}`);
7030
7081
  }
7031
7082
  });
7032
- kms.command("grant").description("grant a principal use of a key's current version").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>", "grant an org member").option("--token <tokenId>", "grant a service token").action(async (options) => {
7083
+ kms.command("grant").description("grant a principal use of a key's current version").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <email>", "grant an org member").option("--token <tokenId>", "grant a service token").action(async (options) => {
7033
7084
  if (!options.user === !options.token) fail("pass exactly one of --user or --token");
7034
7085
  const ctx = buildContext();
7035
7086
  const org = await resolveOrg(ctx, options.org);
@@ -7043,19 +7094,20 @@ function registerKmsCommands(program) {
7043
7094
  });
7044
7095
  console.error(`granted ${key.name} to ${recipient.principalId}`);
7045
7096
  });
7046
- kms.command("revoke").description("revoke a principal from a key (all versions)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>").option("--token <tokenId>").action(async (options) => {
7097
+ kms.command("revoke").description("revoke a principal from a key (all versions)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <email>", "revoke this user's grant").option("--token <tokenId>", "revoke this service token's grant").option("-y, --yes", "skip the confirmation").action(async (options) => {
7047
7098
  if (!options.user === !options.token) fail("pass exactly one of --user or --token");
7048
7099
  const ctx = buildContext();
7049
7100
  const org = await resolveOrg(ctx, options.org);
7050
7101
  const key = await kmsResolveKey(ctx, org.id, options.key);
7051
7102
  const recipient = await kmsResolveRecipient(ctx, org.id, options);
7103
+ await confirmDestructive(options.yes, `Revoke ${recipient.principalId} from ${key.name}? Anything already decrypted stays decrypted.`);
7052
7104
  await ctx.client.revokeKmsKey(org.id, key.id, {
7053
7105
  principalType: recipient.principalType,
7054
7106
  principalId: recipient.principalId
7055
7107
  });
7056
7108
  console.error(`revoked ${recipient.principalId} from ${key.name}`);
7057
7109
  });
7058
- kms.command("rotate").description("add a new key version and re-wrap it for every current grantee").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7110
+ kms.command("rotate").description("add a new key version and re-wrap it for every current grantee").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7059
7111
  const ctx = buildContext();
7060
7112
  const org = await resolveOrg(ctx, options.org);
7061
7113
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7089,21 +7141,23 @@ function registerKmsCommands(program) {
7089
7141
  });
7090
7142
  console.error(`rotated ${rotated.name} to v${rotated.currentVersion} (${grants.length} grantees)`);
7091
7143
  });
7092
- kms.command("disable").description("disable a key (blocks all use: encrypt/decrypt/sign and new grants/rotations)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7144
+ kms.command("disable").description("disable a key (blocks all use: encrypt/decrypt/sign and new grants/rotations)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
7093
7145
  const ctx = buildContext();
7094
7146
  const org = await resolveOrg(ctx, options.org);
7095
7147
  const key = await kmsResolveKey(ctx, org.id, options.key);
7148
+ await confirmDestructive(options.yes, `Disable ${key.name}? Every encrypt, decrypt, and sign against it starts failing.`);
7096
7149
  await ctx.client.disableKmsKey(org.id, key.id);
7097
7150
  console.error(`disabled ${key.name}`);
7098
7151
  });
7099
- kms.command("delete").description("delete a key (hides it from all listings; the name frees up for reuse)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7152
+ kms.command("delete").description("delete a key (hides it from all listings; the name frees up for reuse)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
7100
7153
  const ctx = buildContext();
7101
7154
  const org = await resolveOrg(ctx, options.org);
7102
7155
  const key = await kmsResolveKey(ctx, org.id, options.key);
7156
+ await confirmDestructive(options.yes, `Delete ${key.name}? Any ciphertext or signature still relying on it becomes unrecoverable.`);
7103
7157
  await ctx.client.deleteKmsKey(org.id, key.id);
7104
7158
  console.error(`deleted ${key.name}`);
7105
7159
  });
7106
- kms.command("encrypt").description("encrypt stdin under a key (prints a ce1 ciphertext blob)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "encryption context bound as AAD (required identically to decrypt)").action(async (options) => {
7160
+ kms.command("encrypt").description("encrypt stdin under a key (prints a ce1 ciphertext blob)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--context <ctx>", "encryption context bound as AAD (required identically to decrypt)").action(async (options) => {
7107
7161
  const ctx = buildContext();
7108
7162
  const org = await resolveOrg(ctx, options.org);
7109
7163
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7116,7 +7170,7 @@ function registerKmsCommands(program) {
7116
7170
  }, plaintext, options.context ?? "");
7117
7171
  console.log(blob);
7118
7172
  });
7119
- kms.command("decrypt").description("decrypt a ce1 blob from stdin").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "the same encryption context used to encrypt").action(async (options) => {
7173
+ kms.command("decrypt").description("decrypt a ce1 blob from stdin").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--context <ctx>", "the same encryption context used to encrypt").action(async (options) => {
7120
7174
  const ctx = buildContext();
7121
7175
  const org = await resolveOrg(ctx, options.org);
7122
7176
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7125,7 +7179,7 @@ function registerKmsCommands(program) {
7125
7179
  const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
7126
7180
  console.log(await kmsDecrypt(material, blob, options.context ?? ""));
7127
7181
  });
7128
- kms.command("generate-data-key").description("generate a data key: prints JSON {plaintextBase64, wrapped}").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7182
+ kms.command("generate-data-key").description("generate a data key: prints JSON {plaintextBase64, wrapped}").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7129
7183
  const ctx = buildContext();
7130
7184
  const org = await resolveOrg(ctx, options.org);
7131
7185
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7140,7 +7194,7 @@ function registerKmsCommands(program) {
7140
7194
  wrapped: dk.wrapped
7141
7195
  }));
7142
7196
  });
7143
- kms.command("open-data-key").description("recover a data key from a dk1 blob on stdin (prints plaintext base64)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7197
+ kms.command("open-data-key").description("recover a data key from a dk1 blob on stdin (prints plaintext base64)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7144
7198
  const ctx = buildContext();
7145
7199
  const org = await resolveOrg(ctx, options.org);
7146
7200
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7149,7 +7203,7 @@ function registerKmsCommands(program) {
7149
7203
  const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
7150
7204
  console.log(toBase64(await decryptDataKey(material, wrapped)));
7151
7205
  });
7152
- kms.command("sign").description("sign stdin with a signing key (prints an sg1 signature)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
7206
+ kms.command("sign").description("sign stdin with a signing key (prints an sg1 signature)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7153
7207
  const ctx = buildContext();
7154
7208
  const org = await resolveOrg(ctx, options.org);
7155
7209
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7162,7 +7216,7 @@ function registerKmsCommands(program) {
7162
7216
  version: currentVersion
7163
7217
  }, message));
7164
7218
  });
7165
- kms.command("verify").description("verify an sg1 signature over stdin (exit 0 = valid)").requiredOption("--key <name>", "key name or id").requiredOption("--signature <sg1>", "the signature blob").option("--org <slug>").action(async (options) => {
7219
+ kms.command("verify").description("verify an sg1 signature over stdin (exit 0 = valid)").requiredOption("--key <name>", "key name or id").requiredOption("--signature <sg1>", "the signature blob").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7166
7220
  const ctx = buildContext();
7167
7221
  const org = await resolveOrg(ctx, options.org);
7168
7222
  const key = await kmsResolveKey(ctx, org.id, options.key);
@@ -7264,7 +7318,7 @@ async function buildRecoveryConfig(ctx, orgId, thresholdRaw, custodianRefs) {
7264
7318
  }
7265
7319
  function registerRecoveryCommands(program) {
7266
7320
  const recovery = program.command("recovery").description("customer-controlled M-of-N recovery (zero-knowledge)");
7267
- recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>").action(async (options) => {
7321
+ recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7268
7322
  const ctx = buildContext();
7269
7323
  const org = await resolveOrg(ctx, options.org);
7270
7324
  const { recovery: status } = await ctx.client.getRecovery(org.id);
@@ -7278,7 +7332,7 @@ function registerRecoveryCommands(program) {
7278
7332
  for (const cst of status.custodians) console.log(` - ${cst.label ?? cst.principalId} (${cst.principalType}, share #${cst.shareIndex})`);
7279
7333
  if (status.coverage.unprotectedEnvIds.length > 0) console.log(`${status.coverage.unprotectedEnvIds.length} environment(s) not yet protected — run \`seekrit recovery sync\``);
7280
7334
  });
7281
- recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>").action(async (options) => {
7335
+ recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7282
7336
  const ctx = buildContext();
7283
7337
  const org = await resolveOrg(ctx, options.org);
7284
7338
  const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
@@ -7291,12 +7345,12 @@ function registerRecoveryCommands(program) {
7291
7345
  console.error(`recovery-protected ${wrapped} environment(s) you can decrypt`);
7292
7346
  if (skipped > 0) console.error(`${skipped} environment(s) need another admin to run \`seekrit recovery sync\``);
7293
7347
  });
7294
- recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>").action(async (options) => {
7348
+ recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7295
7349
  const ctx = buildContext();
7296
7350
  const { wrapped, skipped } = await syncRecoveryGrants(ctx, (await resolveOrg(ctx, options.org)).id);
7297
7351
  console.error(`recovery-protected ${wrapped} environment(s); skipped ${skipped} you cannot decrypt`);
7298
7352
  });
7299
- recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>").action(async (options) => {
7353
+ recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7300
7354
  const ctx = buildContext();
7301
7355
  const org = await resolveOrg(ctx, options.org);
7302
7356
  const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
@@ -7309,13 +7363,14 @@ function registerRecoveryCommands(program) {
7309
7363
  console.error(`re-wrapped ${wrapped} environment(s) you can decrypt to the new recovery key`);
7310
7364
  if (skipped > 0) console.error(`${skipped} environment(s) still need another admin to run \`seekrit recovery sync\``);
7311
7365
  });
7312
- recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>").action(async (options) => {
7366
+ recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
7313
7367
  const ctx = buildContext();
7314
7368
  const org = await resolveOrg(ctx, options.org);
7369
+ await confirmDestructive(options.yes, `Disable recovery for ${org.slug}? Every custodian share is dropped, and the org loses its break-glass path.`);
7315
7370
  await ctx.client.disableRecovery(org.id);
7316
7371
  console.error("recovery disabled; recovery grants removed");
7317
7372
  });
7318
- recovery.command("request").description("start a recovery ceremony (defaults to recovering access for yourself)").option("--target-user <email>", "recover access for another member").option("--target-token <id>", "recover access for a service token").option("--reason <text>", "note recorded in the audit trail").option("--org <slug>").action(async (options) => {
7373
+ recovery.command("request").description("start a recovery ceremony (defaults to recovering access for yourself)").option("--target-user <email>", "recover access for another member").option("--target-token <id>", "recover access for a service token").option("--reason <text>", "note recorded in the audit trail").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7319
7374
  const ctx = buildContext();
7320
7375
  const org = await resolveOrg(ctx, options.org);
7321
7376
  const target = options.targetUser || options.targetToken ? await kmsResolveRecipient(ctx, org.id, {
@@ -7332,7 +7387,7 @@ function registerRecoveryCommands(program) {
7332
7387
  console.error(` custodians run: seekrit recovery approve ${request.id}`);
7333
7388
  console.error(` then the target: seekrit recovery complete ${request.id}`);
7334
7389
  });
7335
- recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
7390
+ recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (requestId, options) => {
7336
7391
  const ctx = buildContext();
7337
7392
  const org = await resolveOrg(ctx, options.org);
7338
7393
  const { request } = await ctx.client.getRecoveryRequest(org.id, requestId);
@@ -7345,7 +7400,7 @@ function registerRecoveryCommands(program) {
7345
7400
  });
7346
7401
  console.error(`contributed share #${myShare.shareIndex}: ${res.contributed}/${res.threshold} collected${res.quorumReached ? " — quorum reached" : ""}`);
7347
7402
  });
7348
- recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
7403
+ recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (requestId, options) => {
7349
7404
  const ctx = buildContext();
7350
7405
  const org = await resolveOrg(ctx, options.org);
7351
7406
  const { request, contributions, quorumReached } = await ctx.client.getRecoveryRequest(org.id, requestId);
@@ -7369,9 +7424,10 @@ function registerRecoveryCommands(program) {
7369
7424
  });
7370
7425
  console.error(`recovery complete: restored access to ${restored.length} environment(s)`);
7371
7426
  });
7372
- recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
7427
+ recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (requestId, options) => {
7373
7428
  const ctx = buildContext();
7374
7429
  const org = await resolveOrg(ctx, options.org);
7430
+ await confirmDestructive(options.yes, `Cancel recovery request ${requestId}? Approvals already collected are discarded.`);
7375
7431
  await ctx.client.cancelRecoveryRequest(org.id, requestId);
7376
7432
  console.error(`recovery request ${requestId} canceled`);
7377
7433
  });
@@ -7385,7 +7441,7 @@ function registerRecoveryCommands(program) {
7385
7441
  */
7386
7442
  function registerAppCommands(program) {
7387
7443
  const app = program.command("app").description("manage applications");
7388
- app.command("list").alias("ls").description("list applications in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
7444
+ app.command("list").alias("ls").description("list applications in an organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
7389
7445
  const ctx = buildContext();
7390
7446
  const ref = await resolveOrg(ctx, options.org);
7391
7447
  const { apps } = await ctx.client.listApps(ref.id);
@@ -7396,7 +7452,7 @@ function registerAppCommands(program) {
7396
7452
  col("id", (a) => a.id)
7397
7453
  ], "no applications — create one with `seekrit app create`"));
7398
7454
  });
7399
- app.command("show [slug]").description("show an application, its environments, and your access to each").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (slug, options) => {
7455
+ app.command("show [slug]").description("show an application, its environments, and your access to each").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (slug, options) => {
7400
7456
  const ctx = buildContext();
7401
7457
  const ref = await resolveApp(ctx, {
7402
7458
  org: options.org,
@@ -7432,7 +7488,7 @@ function registerAppCommands(program) {
7432
7488
  }
7433
7489
  });
7434
7490
  });
7435
- app.command("create").description("create an application").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
7491
+ app.command("create").description("create an application").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
7436
7492
  const ctx = buildContext();
7437
7493
  const orgRef = await resolveOrg(ctx, options.org);
7438
7494
  const created = await ctx.client.createApp(orgRef.id, {
@@ -7441,7 +7497,7 @@ function registerAppCommands(program) {
7441
7497
  });
7442
7498
  console.error(`created app ${created.app.slug} (${created.app.id})`);
7443
7499
  });
7444
- app.command("rename [slug]").description("change an application's display name (the slug is permanent)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
7500
+ app.command("rename [slug]").description("change an application's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
7445
7501
  const ctx = buildContext();
7446
7502
  const ref = await resolveApp(ctx, {
7447
7503
  org: options.org,
@@ -7450,7 +7506,7 @@ function registerAppCommands(program) {
7450
7506
  const { app: row } = await ctx.client.updateApp(ref.orgId, ref.id, { name: options.name });
7451
7507
  console.error(`renamed ${row.slug} to "${row.name}"`);
7452
7508
  });
7453
- app.command("rm <slug>").alias("delete").description("delete an application and every environment and secret in it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
7509
+ app.command("rm <slug>").alias("delete").description("delete an application and every environment and secret in it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (slug, options) => {
7454
7510
  const ctx = buildContext();
7455
7511
  const ref = await resolveApp(ctx, {
7456
7512
  org: options.org,
@@ -7462,7 +7518,7 @@ function registerAppCommands(program) {
7462
7518
  console.error(`deleted app ${ref.slug}`);
7463
7519
  });
7464
7520
  const env = program.command("env").description("manage environments");
7465
- env.command("list").alias("ls").description("list an application's environments").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (options) => {
7521
+ env.command("list").alias("ls").description("list an application's environments").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (options) => {
7466
7522
  const ctx = buildContext();
7467
7523
  const ref = await resolveApp(ctx, options);
7468
7524
  const { environments } = await ctx.client.getApp(ref.orgId, ref.id);
@@ -7473,7 +7529,7 @@ function registerAppCommands(program) {
7473
7529
  col("id", (e) => e.id)
7474
7530
  ], "no environments — create one with `seekrit env create`"));
7475
7531
  });
7476
- env.command("show").description("show one environment: composed groups, who holds a key, secret count").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
7532
+ env.command("show").description("show one environment: composed groups, who holds a key, secret count").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
7477
7533
  const ctx = buildContext();
7478
7534
  const target = await resolveAppEnv(ctx, options);
7479
7535
  const [{ environment }, { groups }, { secrets }, { branches }] = await Promise.all([
@@ -7513,7 +7569,7 @@ function registerAppCommands(program) {
7513
7569
  }
7514
7570
  });
7515
7571
  });
7516
- env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
7572
+ env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
7517
7573
  const ctx = buildContext();
7518
7574
  const orgRef = await resolveOrg(ctx, options.org);
7519
7575
  const { apps } = await ctx.client.listApps(orgRef.id);
@@ -7532,7 +7588,7 @@ function registerAppCommands(program) {
7532
7588
  });
7533
7589
  console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
7534
7590
  });
7535
- env.command("rm").alias("delete").description("delete an environment and every secret in it").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "delete a group environment instead of an app one").requiredOption("--env <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
7591
+ env.command("rm").alias("delete").description("delete an environment and every secret in it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "delete a group environment instead of an app one").requiredOption("--env <slug>", "environment slug").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
7536
7592
  const ctx = buildContext();
7537
7593
  const target = await resolveEnvTarget(ctx, options);
7538
7594
  const { secrets } = await ctx.client.listSecrets(target.orgId, target.envId);
@@ -7541,7 +7597,7 @@ function registerAppCommands(program) {
7541
7597
  console.error(`deleted ${target.label}`);
7542
7598
  });
7543
7599
  const envGroups = env.command("groups").description("compose shared groups into an application environment");
7544
- envGroups.command("add").description("compose a group into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
7600
+ envGroups.command("add").description("compose a group into an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").requiredOption("--group <slug>", "group slug").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
7545
7601
  const ctx = buildContext();
7546
7602
  const target = await resolveAppEnv(ctx, options);
7547
7603
  const group = await resolveGroup(ctx, {
@@ -7554,7 +7610,7 @@ function registerAppCommands(program) {
7554
7610
  });
7555
7611
  console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
7556
7612
  });
7557
- envGroups.command("list").alias("ls").description("list groups composed into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
7613
+ envGroups.command("list").alias("ls").description("list groups composed into an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
7558
7614
  const ctx = buildContext();
7559
7615
  const target = await resolveAppEnv(ctx, options);
7560
7616
  const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
@@ -7564,13 +7620,14 @@ function registerAppCommands(program) {
7564
7620
  col("name", (g) => g.name)
7565
7621
  ], "no groups composed into this environment"));
7566
7622
  });
7567
- envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").action(async (options) => {
7623
+ envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").requiredOption("--group <slug>", "group slug").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
7568
7624
  const ctx = buildContext();
7569
7625
  const target = await resolveAppEnv(ctx, options);
7570
7626
  const group = await resolveGroup(ctx, {
7571
7627
  org: options.org,
7572
7628
  group: options.group
7573
7629
  });
7630
+ await confirmDestructive(options.yes, `Remove ${group.slug} from ${target.appSlug}/${target.envSlug}? Everything it contributed disappears from that environment.`);
7574
7631
  await ctx.client.unlinkEnvGroup(target.orgId, target.envId, group.id);
7575
7632
  console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
7576
7633
  });
@@ -8593,7 +8650,7 @@ async function decryptArchive(archive, key, filter) {
8593
8650
  }
8594
8651
  function registerArchiveCommands(program) {
8595
8652
  const archive = program.command("archive").description("export the whole org as one signed file, and open it offline");
8596
- archive.command("create").description("download a signed archive of everything seekrit stores for the org").option("--org <slug>").option("-o, --out <file>", "write the archive here (default: ./seekrit-<org>-<date>.json)").option("--no-versions", "omit each secret's ciphertext history").option("--no-audit", "omit the audit trail").option("--audit-limit <n>", "keep at most this many of the newest audit rows", Number).option("--json", "print the archive to stdout instead of writing a file").action(async (options) => {
8653
+ archive.command("create").description("download a signed archive of everything seekrit stores for the org").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-o, --out <file>", "write the archive here (default: ./seekrit-<org>-<date>.json)").option("--no-versions", "omit each secret's ciphertext history").option("--no-audit", "omit the audit trail").option("--audit-limit <n>", "keep at most this many of the newest audit rows", Number).option("--json", "print the archive to stdout instead of writing a file").action(async (options) => {
8597
8654
  const ctx = buildContext();
8598
8655
  const ref = await resolveOrg(ctx, options.org);
8599
8656
  const input = {
@@ -8706,7 +8763,7 @@ function registerArchiveCommands(program) {
8706
8763
  } catch {}
8707
8764
  fail("none of the recovery shares in this archive unwrap with that key");
8708
8765
  });
8709
- archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$5).option("--yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
8766
+ archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$5).option("-y, --yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
8710
8767
  if (!options.out && !options.stdout) fail("choose a destination: --out <dir> to write files, or --stdout to print plaintext");
8711
8768
  if (![
8712
8769
  "dotenv",
@@ -8764,7 +8821,7 @@ function parseLimit(raw) {
8764
8821
  */
8765
8822
  function registerAuditCommands(program) {
8766
8823
  const audit = program.command("audit").description("read the org audit trail");
8767
- audit.command("list", { isDefault: true }).alias("ls").description("show the org audit trail").option("--org <slug>").option("--limit <n>", `entries per page (max ${MAX_PAGE})`, "50").option("--action <action>", "only this action, e.g. env.key_granted").option("--resource-type <type>", "only this resource type, e.g. environment").option("--cursor <cursor>", "continue from a previous page's cursor").option("--all", "page through the whole trail, not just the first page").option("--metadata", "include each entry's metadata as JSON").option("--json", "print the raw API response").action(async (options) => {
8824
+ audit.command("list", { isDefault: true }).alias("ls").description("show the org audit trail").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--limit <n>", `entries per page (max ${MAX_PAGE})`, "50").option("--action <action>", "only this action, e.g. env.key_granted").option("--resource-type <type>", "only this resource type, e.g. environment").option("--cursor <cursor>", "continue from a previous page's cursor").option("--all", "page through the whole trail, not just the first page").option("--metadata", "include each entry's metadata as JSON").option("--json", "print the raw API response").action(async (options) => {
8768
8825
  if (options.action && !AUDIT_ACTIONS.includes(options.action)) fail(`unknown action "${options.action}" — see \`seekrit audit actions\``);
8769
8826
  const ctx = buildContext();
8770
8827
  const ref = await resolveOrg(ctx, options.org);
@@ -8847,7 +8904,7 @@ function resolveBaseCredential(opts) {
8847
8904
  function registerAwsCommands(program) {
8848
8905
  const aws = program.command("aws").description("temporary AWS credentials (STS AssumeRole, zero-knowledge)");
8849
8906
  const target = aws.command("target").description("manage AWS role targets");
8850
- target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
8907
+ target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
8851
8908
  const ctx = buildContext();
8852
8909
  const org = await resolveOrg(ctx, options.org);
8853
8910
  const sessionPolicy = options.sessionPolicy ? readFileSync(options.sessionPolicy, "utf8").trim() : void 0;
@@ -8872,7 +8929,7 @@ function registerAwsCommands(program) {
8872
8929
  console.error("\nEnsure the role trusts the admin principal, then `seekrit aws lease`:\n");
8873
8930
  console.log(awsTrustPolicyInstructions(config));
8874
8931
  });
8875
- target.command("list").description("list AWS role targets").option("--org <slug>").action(async (options) => {
8932
+ target.command("list").description("list AWS role targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
8876
8933
  const ctx = buildContext();
8877
8934
  const org = await resolveOrg(ctx, options.org);
8878
8935
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -8882,7 +8939,7 @@ function registerAwsCommands(program) {
8882
8939
  console.log(`${t.id}\t${t.name}\t${cfg.region}\t${cfg.roleArn}`);
8883
8940
  }
8884
8941
  });
8885
- target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>").action(async (targetId, options) => {
8942
+ target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
8886
8943
  const ctx = buildContext();
8887
8944
  const org = await resolveOrg(ctx, options.org);
8888
8945
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -8892,13 +8949,14 @@ function registerAwsCommands(program) {
8892
8949
  if (cfg.provider !== "aws") fail("not an aws target (see `seekrit pg`/`seekrit ssh`)");
8893
8950
  console.log(awsTrustPolicyInstructions(cfg));
8894
8951
  });
8895
- target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>").action(async (targetId, options) => {
8952
+ target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
8896
8953
  const ctx = buildContext();
8897
8954
  const org = await resolveOrg(ctx, options.org);
8955
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
8898
8956
  await ctx.client.deleteLeaseTarget(org.id, targetId);
8899
8957
  console.error(`deleted ${targetId}`);
8900
8958
  });
8901
- aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
8959
+ aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
8902
8960
  const ctx = buildContext();
8903
8961
  const org = await resolveOrg(ctx, options.org);
8904
8962
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -8928,7 +8986,7 @@ function registerAwsCommands(program) {
8928
8986
  console.log(`export AWS_REGION=${cred.region}`);
8929
8987
  }
8930
8988
  });
8931
- aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
8989
+ aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
8932
8990
  const ctx = buildContext();
8933
8991
  const org = await resolveOrg(ctx, options.org);
8934
8992
  const { leases } = await ctx.client.listLeases(org.id);
@@ -8937,9 +8995,10 @@ function registerAwsCommands(program) {
8937
8995
  console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
8938
8996
  }
8939
8997
  });
8940
- aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
8998
+ aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
8941
8999
  const ctx = buildContext();
8942
9000
  const org = await resolveOrg(ctx, options.org);
9001
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Credentials already issued stay valid until they expire.`);
8943
9002
  await ctx.client.revokeLease(org.id, leaseId);
8944
9003
  console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
8945
9004
  });
@@ -8960,7 +9019,7 @@ function usageLine(usage) {
8960
9019
  */
8961
9020
  function registerBillingCommands(program) {
8962
9021
  const billing = program.command("billing").description("plan, usage, and subscription");
8963
- billing.command("show", { isDefault: true }).description("show the org's plan, what it includes, and current usage").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9022
+ billing.command("show", { isDefault: true }).description("show the org's plan, what it includes, and current usage").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
8964
9023
  const ctx = buildContext();
8965
9024
  const ref = await resolveOrg(ctx, options.org);
8966
9025
  const info = await ctx.client.getBilling(ref.id);
@@ -8987,7 +9046,7 @@ function registerBillingCommands(program) {
8987
9046
  }
8988
9047
  });
8989
9048
  });
8990
- billing.command("entitlements").description("list every entitlement this org resolves to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9049
+ billing.command("entitlements").description("list every entitlement this org resolves to").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
8991
9050
  const ctx = buildContext();
8992
9051
  const ref = await resolveOrg(ctx, options.org);
8993
9052
  const info = await ctx.client.getBilling(ref.id);
@@ -8997,7 +9056,7 @@ function registerBillingCommands(program) {
8997
9056
  col("source", (e) => e.source)
8998
9057
  ]));
8999
9058
  });
9000
- billing.command("checkout <family>").description(`start a self-serve upgrade (${VISIBLE_PLAN_FAMILY_IDS.join(" | ")}) — prints a URL`).option("--org <slug>").action(async (family, options) => {
9059
+ billing.command("checkout <family>").description(`start a self-serve upgrade (${VISIBLE_PLAN_FAMILY_IDS.join(" | ")}) — prints a URL`).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (family, options) => {
9001
9060
  if (!PLAN_FAMILY_IDS.includes(family)) fail(`unknown plan "${family}" — one of: ${PLAN_FAMILY_IDS.join(", ")}`);
9002
9061
  const ctx = buildContext();
9003
9062
  const ref = await resolveOrg(ctx, options.org);
@@ -9005,14 +9064,14 @@ function registerBillingCommands(program) {
9005
9064
  console.error("open this to complete checkout:");
9006
9065
  console.log(url);
9007
9066
  });
9008
- billing.command("portal").description("open the billing portal (prints a URL) to manage payment and invoices").option("--org <slug>").action(async (options) => {
9067
+ billing.command("portal").description("open the billing portal (prints a URL) to manage payment and invoices").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9009
9068
  const ctx = buildContext();
9010
9069
  const ref = await resolveOrg(ctx, options.org);
9011
9070
  const { url } = await ctx.client.openBillingPortal(ref.id);
9012
9071
  console.error("open this to manage billing:");
9013
9072
  console.log(url);
9014
9073
  });
9015
- billing.command("cancel").description("cancel the subscription and drop back to the Free plan").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
9074
+ billing.command("cancel").description("cancel the subscription and drop back to the Free plan").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
9016
9075
  const ctx = buildContext();
9017
9076
  const ref = await resolveOrg(ctx, options.org);
9018
9077
  await confirmDestructive(options.yes, `Cancel ${ref.slug}'s subscription and move it to the Free plan?`);
@@ -9055,7 +9114,7 @@ async function resolveBranchParent(ctx, opts) {
9055
9114
  */
9056
9115
  function registerBranchCommands(program) {
9057
9116
  const branch = program.command("branch").description("ephemeral per-PR / preview configs layered on an environment");
9058
- branch.command("create <slug>").description("fork an environment into an ephemeral branch (inherits its secrets)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--from <env>", "the environment to branch").option("--name <name>", "display name (defaults to the slug)").option("--ttl <duration>", "lifetime: 12h, 7d, 2w, … or `never`", "7d").option("--no-share", "don't give the parent's other readers access to this branch").action(async (slug, options) => {
9117
+ branch.command("create <slug>").description("fork an environment into an ephemeral branch (inherits its secrets)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--from <env>", "the environment to branch").option("--name <name>", "display name (defaults to the slug)").option("--ttl <duration>", "lifetime: 12h, 7d, 2w, … or `never`", "7d").option("--no-share", "don't give the parent's other readers access to this branch").action(async (slug, options) => {
9059
9118
  const ctx = buildContext();
9060
9119
  const parent = await resolveBranchParent(ctx, {
9061
9120
  org: options.org,
@@ -9093,7 +9152,7 @@ function registerBranchCommands(program) {
9093
9152
  console.error(created.branch.expiresAt ? `expires ${created.branch.expiresAt}` : "no expiry — delete it explicitly when the PR closes");
9094
9153
  if (grants.length > 0) console.error(`shared with ${grants.length} other reader(s)`);
9095
9154
  });
9096
- branch.command("list").alias("ls").description("list branches in an application (or of one environment)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").option("--json", "print the raw API response").action(async (options) => {
9155
+ branch.command("list").alias("ls").description("list branches in an application (or of one environment)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").option("--json", "print the raw API response").action(async (options) => {
9097
9156
  const ctx = buildContext();
9098
9157
  const branches = options.env ? await (async () => {
9099
9158
  const parent = await resolveAppEnv(ctx, options);
@@ -9109,10 +9168,11 @@ function registerBranchCommands(program) {
9109
9168
  col("id", (b) => b.id)
9110
9169
  ], "no branches — create one with `seekrit branch create`"));
9111
9170
  });
9112
- branch.command("delete <slug>").alias("rm").description("tear down a branch and everything it overrode").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").action(async (slug, options) => {
9171
+ branch.command("delete <slug>").alias("rm").description("tear down a branch and everything it overrode").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("-y, --yes", "skip the confirmation").action(async (slug, options) => {
9113
9172
  const ctx = buildContext();
9114
9173
  const app = await resolveApp(ctx, options);
9115
9174
  const target = await resolveBranch(ctx, app, slug);
9175
+ await confirmDestructive(options.yes, `Tear down branch ${app.slug}#${target.slug} and every override in it?`);
9116
9176
  await ctx.client.deleteBranch(app.orgId, target.id);
9117
9177
  console.error(`deleted branch ${app.slug}#${target.slug}`);
9118
9178
  });
@@ -9325,7 +9385,7 @@ function resolveServiceAccountKey(opts) {
9325
9385
  function registerGcpCommands(program) {
9326
9386
  const gcp = program.command("gcp").description("temporary GCP credentials (IAM generateAccessToken, zero-knowledge)");
9327
9387
  const target = gcp.command("target").description("manage GCP service-account targets");
9328
- target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
9388
+ target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
9329
9389
  const ctx = buildContext();
9330
9390
  const org = await resolveOrg(ctx, options.org);
9331
9391
  const config = {
@@ -9348,7 +9408,7 @@ function registerGcpCommands(program) {
9348
9408
  console.error("\nGrant the source SA the token-creator role, then `seekrit gcp lease`:\n");
9349
9409
  console.log(gcpSetupInstructions(config));
9350
9410
  });
9351
- target.command("list").description("list GCP service-account targets").option("--org <slug>").action(async (options) => {
9411
+ target.command("list").description("list GCP service-account targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9352
9412
  const ctx = buildContext();
9353
9413
  const org = await resolveOrg(ctx, options.org);
9354
9414
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9358,7 +9418,7 @@ function registerGcpCommands(program) {
9358
9418
  console.log(`${t.id}\t${t.name}\t${cfg.serviceAccount}`);
9359
9419
  }
9360
9420
  });
9361
- target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>").action(async (targetId, options) => {
9421
+ target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
9362
9422
  const ctx = buildContext();
9363
9423
  const org = await resolveOrg(ctx, options.org);
9364
9424
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9368,13 +9428,14 @@ function registerGcpCommands(program) {
9368
9428
  if (cfg.provider !== "gcp") fail("not a gcp target (see `seekrit aws`/`seekrit ssh`)");
9369
9429
  console.log(gcpSetupInstructions(cfg));
9370
9430
  });
9371
- target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>").action(async (targetId, options) => {
9431
+ target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
9372
9432
  const ctx = buildContext();
9373
9433
  const org = await resolveOrg(ctx, options.org);
9434
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
9374
9435
  await ctx.client.deleteLeaseTarget(org.id, targetId);
9375
9436
  console.error(`deleted ${targetId}`);
9376
9437
  });
9377
- gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
9438
+ gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
9378
9439
  const ctx = buildContext();
9379
9440
  const org = await resolveOrg(ctx, options.org);
9380
9441
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9399,7 +9460,7 @@ function registerGcpCommands(program) {
9399
9460
  console.log(`export GOOGLE_OAUTH_ACCESS_TOKEN=${cred.accessToken}`);
9400
9461
  }
9401
9462
  });
9402
- gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
9463
+ gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9403
9464
  const ctx = buildContext();
9404
9465
  const org = await resolveOrg(ctx, options.org);
9405
9466
  const { leases } = await ctx.client.listLeases(org.id);
@@ -9408,9 +9469,10 @@ function registerGcpCommands(program) {
9408
9469
  console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
9409
9470
  }
9410
9471
  });
9411
- gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
9472
+ gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
9412
9473
  const ctx = buildContext();
9413
9474
  const org = await resolveOrg(ctx, options.org);
9475
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Tokens already issued stay valid until they expire.`);
9414
9476
  await ctx.client.revokeLease(org.id, leaseId);
9415
9477
  console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
9416
9478
  });
@@ -9424,7 +9486,7 @@ function registerGcpCommands(program) {
9424
9486
  */
9425
9487
  function registerGroupCommands(program) {
9426
9488
  const group = program.command("group").description("manage shared groups (reusable secret bags)");
9427
- group.command("list").alias("ls").description("list shared groups in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9489
+ group.command("list").alias("ls").description("list shared groups in an organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
9428
9490
  const ctx = buildContext();
9429
9491
  const ref = await resolveOrg(ctx, options.org);
9430
9492
  const { groups } = await ctx.client.listGroups(ref.id);
@@ -9435,7 +9497,7 @@ function registerGroupCommands(program) {
9435
9497
  col("id", (g) => g.id)
9436
9498
  ], "no groups — create one with `seekrit group create`"));
9437
9499
  });
9438
- group.command("show <slug>").description("show a group and the environments (value sets) it holds").option("--org <slug>").option("--json", "print the raw API response").action(async (slug, options) => {
9500
+ group.command("show <slug>").description("show a group and the environments (value sets) it holds").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (slug, options) => {
9439
9501
  const ctx = buildContext();
9440
9502
  const ref = await resolveGroup(ctx, {
9441
9503
  org: options.org,
@@ -9461,7 +9523,7 @@ function registerGroupCommands(program) {
9461
9523
  ], "no environments — create one with `seekrit group env create`");
9462
9524
  });
9463
9525
  });
9464
- group.command("create").description("create a shared group").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
9526
+ group.command("create").description("create a shared group").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
9465
9527
  const ctx = buildContext();
9466
9528
  const orgRef = await resolveOrg(ctx, options.org);
9467
9529
  const created = await ctx.client.createGroup(orgRef.id, {
@@ -9470,7 +9532,7 @@ function registerGroupCommands(program) {
9470
9532
  });
9471
9533
  console.error(`created group ${created.group.slug} (${created.group.id})`);
9472
9534
  });
9473
- group.command("rename <slug>").description("change a group's display name (the slug is permanent)").option("--org <slug>").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
9535
+ group.command("rename <slug>").description("change a group's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
9474
9536
  const ctx = buildContext();
9475
9537
  const ref = await resolveGroup(ctx, {
9476
9538
  org: options.org,
@@ -9479,7 +9541,7 @@ function registerGroupCommands(program) {
9479
9541
  const { group: row } = await ctx.client.updateGroup(ref.orgId, ref.id, { name: options.name });
9480
9542
  console.error(`renamed ${row.slug} to "${row.name}"`);
9481
9543
  });
9482
- group.command("rm <slug>").alias("delete").description("delete a group, its environments, and their secrets").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
9544
+ group.command("rm <slug>").alias("delete").description("delete a group, its environments, and their secrets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (slug, options) => {
9483
9545
  const ctx = buildContext();
9484
9546
  const ref = await resolveGroup(ctx, {
9485
9547
  org: options.org,
@@ -9491,7 +9553,7 @@ function registerGroupCommands(program) {
9491
9553
  console.error(`deleted group ${ref.slug}`);
9492
9554
  });
9493
9555
  const groupEnv = group.command("env").description("manage a group’s environments (per-slug value sets / variants)");
9494
- groupEnv.command("list").alias("ls").description("list a group's environments").option("--org <slug>").requiredOption("--group <slug>").option("--json", "print the raw API response").action(async (options) => {
9556
+ groupEnv.command("list").alias("ls").description("list a group's environments").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--group <slug>", "group slug").option("--json", "print the raw API response").action(async (options) => {
9495
9557
  const ctx = buildContext();
9496
9558
  const ref = await resolveGroup(ctx, options);
9497
9559
  const { environments } = await ctx.client.listGroupEnvs(ref.orgId, ref.id);
@@ -9502,7 +9564,7 @@ function registerGroupCommands(program) {
9502
9564
  col("id", (e) => e.id)
9503
9565
  ], "no environments — create one with `seekrit group env create`"));
9504
9566
  });
9505
- groupEnv.command("create").description("create a group environment (generates its data key locally)").option("--org <slug>").requiredOption("--group <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
9567
+ groupEnv.command("create").description("create a group environment (generates its data key locally)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--group <slug>", "group slug").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
9506
9568
  const ctx = buildContext();
9507
9569
  const groupRef = await resolveGroup(ctx, {
9508
9570
  org: options.org,
@@ -9523,6 +9585,224 @@ function registerGroupCommands(program) {
9523
9585
  });
9524
9586
  }
9525
9587
  //#endregion
9588
+ //#region src/hermes.ts
9589
+ /**
9590
+ * `seekrit hermes` — wire seekrit into [Hermes Agent](https://hermes-agent.nousresearch.com)
9591
+ * as a **secret source**.
9592
+ *
9593
+ * Hermes reads credentials from `~/.hermes/.env` and the process environment, and
9594
+ * a *secret source* is the documented way to fill that environment from
9595
+ * somewhere else at startup. Implementing one is Python, and it lives in the
9596
+ * Python SDK (`seekrit.hermes`) rather than here — a `SecretSource` subclass and
9597
+ * the `register(ctx)` hook Hermes calls. This command exists for the two parts
9598
+ * that are not Python: the `config.yaml` block, and the plugin directory for a
9599
+ * Hermes install that cannot see the SDK's entry point.
9600
+ *
9601
+ * **`config.yaml` is printed, not written.** It is YAML that belongs to
9602
+ * somebody's agent, with comments and anchors this CLI carries no parser for;
9603
+ * a rewrite through a JSON round trip would delete them. The plugin directory
9604
+ * *is* written, because those two files are entirely ours.
9605
+ *
9606
+ * There are two sources, and which one to use is a real choice rather than a
9607
+ * default. `seekrit` is **bulk**: one environment, whole, nothing to enumerate.
9608
+ * `seekrit_refs` is **mapped**: explicit `VAR: skt://NAME` bindings, which is
9609
+ * what you need to rename a secret, read more than one environment, or win a
9610
+ * contested variable — Hermes lets a mapped claim beat a bulk one.
9611
+ */
9612
+ /** The two source names `seekrit.hermes` registers. */
9613
+ const BULK_SOURCE = "seekrit";
9614
+ const MAPPED_SOURCE = "seekrit_refs";
9615
+ /** Hermes' home: `$HERMES_HOME`, else `~/.hermes`. */
9616
+ function hermesHome(env = process.env) {
9617
+ const home = env.HERMES_HOME?.trim();
9618
+ return home && home.length > 0 ? resolve(home) : join(homedir(), ".hermes");
9619
+ }
9620
+ const NAME_RE$1 = /^[A-Za-z_][A-Za-z0-9_]*$/;
9621
+ /**
9622
+ * Build an `skt://` reference.
9623
+ *
9624
+ * A service token is bound to one environment, so a reference names a *token*
9625
+ * (by the alias its `tokens:` map gives it) and a secret in that token's
9626
+ * environment — not an arbitrary app and env, which nothing on the read path
9627
+ * could resolve.
9628
+ */
9629
+ function reference(name, tokenAlias) {
9630
+ return `skt://${tokenAlias ? `${tokenAlias}/` : ""}${name}`;
9631
+ }
9632
+ /**
9633
+ * The `secrets:` block for `~/.hermes/config.yaml`.
9634
+ *
9635
+ * Hand-rolled rather than serialized, because the shape is fixed and small and
9636
+ * the output is meant to be *read* — a generic emitter would quote keys and
9637
+ * flow-fold lists in ways that make a config someone has to paste look foreign.
9638
+ */
9639
+ function configBlock(options = {}) {
9640
+ const source = options.mapped ? MAPPED_SOURCE : BULK_SOURCE;
9641
+ const lines = [
9642
+ "secrets:",
9643
+ ` sources: [${source}]`,
9644
+ ` ${source}:`,
9645
+ " enabled: true"
9646
+ ];
9647
+ if (options.tokenEnv && options.tokenEnv !== "SEEKRIT_TOKEN") lines.push(` token_env: ${options.tokenEnv}`);
9648
+ if (options.mapped) {
9649
+ const tokens = Object.entries(options.tokens ?? {});
9650
+ if (tokens.length > 0) {
9651
+ lines.push(" tokens:");
9652
+ for (const [alias, variable] of tokens) lines.push(` ${alias}: ${variable}`);
9653
+ }
9654
+ lines.push(" env:");
9655
+ const bindings = options.bindings ?? [];
9656
+ if (bindings.length === 0) lines.push(` OPENAI_API_KEY: ${reference("OPENAI_API_KEY")}`);
9657
+ else for (const b of bindings) lines.push(` ${b.variable}: ${reference(b.name, b.tokenAlias)}`);
9658
+ }
9659
+ return `${lines.join("\n")}\n`;
9660
+ }
9661
+ /** `plugin.yaml` for the directory-install path. */
9662
+ function pluginYaml() {
9663
+ return [
9664
+ "name: seekrit",
9665
+ "description: >-",
9666
+ " Resolve Hermes provider credentials from seekrit — end-to-end encrypted",
9667
+ " secrets, decrypted in this process by your own service token.",
9668
+ ""
9669
+ ].join("\n");
9670
+ }
9671
+ /**
9672
+ * `__init__.py` for the directory-install path.
9673
+ *
9674
+ * Re-exporting `register` is the whole file: the implementation lives in the
9675
+ * published SDK, so a plugin scaffolded once keeps up with SDK releases instead
9676
+ * of pinning a copy of the resolver into somebody's home directory.
9677
+ */
9678
+ function pluginInit() {
9679
+ return [
9680
+ "\"\"\"seekrit secret sources for Hermes Agent.",
9681
+ "",
9682
+ "Hermes calls `register(ctx)` from this module. The implementation lives in",
9683
+ "the `seekrit` package (`pip install seekrit`), so this file stays a re-export",
9684
+ "and never has to be regenerated.",
9685
+ "\"\"\"",
9686
+ "",
9687
+ "from seekrit.hermes import register # noqa: F401",
9688
+ ""
9689
+ ].join("\n");
9690
+ }
9691
+ /**
9692
+ * Parse a `VAR=NAME` or `VAR=alias/NAME` binding for `--bind`.
9693
+ *
9694
+ * The left side is the environment variable Hermes will set; the right side is
9695
+ * the seekrit secret it comes from. They differ often enough — that is most of
9696
+ * why the mapped source exists — that inferring one from the other would be
9697
+ * wrong more than it was convenient.
9698
+ */
9699
+ function parseBinding(raw) {
9700
+ const eq = raw.indexOf("=");
9701
+ if (eq <= 0) return void 0;
9702
+ const variable = raw.slice(0, eq).trim();
9703
+ const target = raw.slice(eq + 1).trim();
9704
+ if (!NAME_RE$1.test(variable) || target.length === 0) return void 0;
9705
+ const slash = target.indexOf("/");
9706
+ if (slash === -1) return NAME_RE$1.test(target) ? {
9707
+ variable,
9708
+ name: target
9709
+ } : void 0;
9710
+ const tokenAlias = target.slice(0, slash).trim();
9711
+ const name = target.slice(slash + 1).trim();
9712
+ if (tokenAlias.length === 0 || !NAME_RE$1.test(name)) return void 0;
9713
+ return {
9714
+ variable,
9715
+ name,
9716
+ tokenAlias
9717
+ };
9718
+ }
9719
+ /** Parse a `--token alias=VAR` pair. */
9720
+ function parseTokenAlias(raw) {
9721
+ const eq = raw.indexOf("=");
9722
+ if (eq <= 0) return void 0;
9723
+ const alias = raw.slice(0, eq).trim();
9724
+ const variable = raw.slice(eq + 1).trim();
9725
+ if (alias.length === 0 || !NAME_RE$1.test(variable)) return void 0;
9726
+ return {
9727
+ alias,
9728
+ variable
9729
+ };
9730
+ }
9731
+ function registerHermesCommands(program) {
9732
+ const hermes = program.command("hermes").description("wire seekrit into a Hermes Agent as a secret source (`seekrit hermes --help`)");
9733
+ hermes.command("init").description("print the Hermes config that resolves credentials from seekrit").option("--mapped", `use the ${MAPPED_SOURCE} source (explicit VAR -> secret bindings)`).option("--token-env <var>", "variable holding the service token", "SEEKRIT_TOKEN").option("--bind <VAR=NAME...>", "a mapped binding, e.g. OPENAI_API_KEY=OPENAI_KEY").option("--token <alias=VAR...>", "name a second token, e.g. billing=SEEKRIT_TOKEN_BILLING").option("--plugin-dir", "also scaffold $HERMES_HOME/plugins/seekrit (for a pip-less install)").option("--home <path>", "Hermes home (default: $HERMES_HOME or ~/.hermes)").option("--json", "machine-readable output").action((options) => {
9734
+ const bindings = (options.bind ?? []).map((raw) => {
9735
+ const parsed = parseBinding(raw);
9736
+ if (!parsed) fail(`--bind must be VAR=NAME or VAR=alias/NAME (got "${raw}")`);
9737
+ return parsed;
9738
+ });
9739
+ const tokens = {};
9740
+ for (const raw of options.token ?? []) {
9741
+ const parsed = parseTokenAlias(raw);
9742
+ if (!parsed) fail(`--token must be alias=VARIABLE (got "${raw}")`);
9743
+ tokens[parsed.alias] = parsed.variable;
9744
+ }
9745
+ for (const binding of bindings) if (binding.tokenAlias && !tokens[binding.tokenAlias]) fail(`--bind ${binding.variable} names token alias "${binding.tokenAlias}", so pass --token ${binding.tokenAlias}=<VARIABLE> too`);
9746
+ if (!options.mapped && (bindings.length > 0 || Object.keys(tokens).length > 0)) fail("--bind and --token describe the mapped source: pass --mapped as well");
9747
+ const source = options.mapped ? MAPPED_SOURCE : BULK_SOURCE;
9748
+ const block = configBlock({
9749
+ mapped: options.mapped,
9750
+ tokenEnv: options.tokenEnv,
9751
+ bindings,
9752
+ tokens
9753
+ });
9754
+ const home = options.home ? resolve(options.home) : hermesHome();
9755
+ const pluginDir = join(home, "plugins", "seekrit");
9756
+ const written = [];
9757
+ if (options.pluginDir) {
9758
+ mkdirSync(pluginDir, { recursive: true });
9759
+ const files = [["plugin.yaml", pluginYaml()], ["__init__.py", pluginInit()]];
9760
+ for (const [name, contents] of files) {
9761
+ const path = join(pluginDir, name);
9762
+ writeFileSync(path, contents);
9763
+ written.push(path);
9764
+ }
9765
+ }
9766
+ emit(options, {
9767
+ source,
9768
+ configPath: join(home, "config.yaml"),
9769
+ block,
9770
+ written
9771
+ }, () => {
9772
+ section("hermes secret source");
9773
+ printFields([
9774
+ ["source", source],
9775
+ ["shape", options.mapped ? "mapped (explicit bindings)" : "bulk (whole environment)"],
9776
+ ["config", join(home, "config.yaml")],
9777
+ ["plugin dir", options.pluginDir ? pluginDir : "not scaffolded"]
9778
+ ]);
9779
+ console.log();
9780
+ console.log("1. Install the SDK into the environment Hermes runs in:");
9781
+ console.log(" pip install seekrit");
9782
+ console.log();
9783
+ console.log("2. Put the service token in ~/.hermes/.env (not config.yaml):");
9784
+ console.log(` ${options.tokenEnv ?? "SEEKRIT_TOKEN"}=skt_...`);
9785
+ console.log();
9786
+ console.log("3. Enable the plugin and merge this into ~/.hermes/config.yaml:");
9787
+ console.log(" hermes plugins enable seekrit");
9788
+ console.log();
9789
+ console.log(block.trimEnd());
9790
+ console.log();
9791
+ if (written.length > 0) {
9792
+ console.log("Scaffolded:");
9793
+ for (const path of written) console.log(` ${path}`);
9794
+ console.log();
9795
+ }
9796
+ console.log("The source overrides values already in .env or your shell, so a rotation wins — list a variable under `preserve_existing` when a local override is the point.");
9797
+ });
9798
+ });
9799
+ hermes.command("ref <name>").description("print the skt:// reference to bind a Hermes variable to").option("--token-alias <alias>", "resolve through a second token named in `tokens:`").option("--json", "machine-readable output").action((name, options) => {
9800
+ if (!NAME_RE$1.test(name)) fail(`not a valid secret name: "${name}" (names are [A-Za-z_][A-Za-z0-9_]*)`);
9801
+ const ref = reference(name, options.tokenAlias);
9802
+ emit(options, { reference: ref }, () => console.log(ref));
9803
+ });
9804
+ }
9805
+ //#endregion
9526
9806
  //#region src/honey.ts
9527
9807
  /**
9528
9808
  * Honey tokens — decoy credentials that unlock nothing and alert when used.
@@ -9534,7 +9814,7 @@ function registerGroupCommands(program) {
9534
9814
  */
9535
9815
  function registerHoneyTokenCommands(program) {
9536
9816
  const honey = program.command("honey-token").description("plant decoy credentials that alert when anyone tries to use them");
9537
- honey.command("create").description("mint a decoy credential; prints it once").requiredOption("--name <name>", "display name, e.g. legacy-ci-bait").option("--org <slug>").option("--placement <note>", "where you're planting it (echoed in the alert email)").action(async (options) => {
9817
+ honey.command("create").description("mint a decoy credential; prints it once").requiredOption("--name <name>", "display name, e.g. legacy-ci-bait").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--placement <note>", "where you're planting it (echoed in the alert email)").action(async (options) => {
9538
9818
  const ctx = buildContext();
9539
9819
  const orgRef = await resolveOrg(ctx, options.org);
9540
9820
  const created = await createHoneyToken();
@@ -9547,7 +9827,7 @@ function registerHoneyTokenCommands(program) {
9547
9827
  console.error("decoy created — save it now, it is not stored. Plant it somewhere a thief would look, NOT anywhere your own tooling reads: a deploy script that tries it by mistake trips the alarm just as loudly. It grants nothing.");
9548
9828
  console.log(created.token);
9549
9829
  });
9550
- honey.command("list").alias("ls").description("list decoy credentials and whether any have been tripped").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9830
+ honey.command("list").alias("ls").description("list decoy credentials and whether any have been tripped").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
9551
9831
  const ctx = buildContext();
9552
9832
  const orgRef = await resolveOrg(ctx, options.org);
9553
9833
  const { honeyTokens } = await ctx.client.listHoneyTokens(orgRef.id);
@@ -9560,7 +9840,7 @@ function registerHoneyTokenCommands(program) {
9560
9840
  col("id", (t) => t.id)
9561
9841
  ], "no decoys planted — create one with `seekrit honey-token create`"));
9562
9842
  });
9563
- honey.command("delete <honeyTokenId>").alias("rm").description("delete a decoy (stops it alerting)").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (honeyTokenId, options) => {
9843
+ honey.command("delete <honeyTokenId>").alias("rm").description("delete a decoy (stops it alerting)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (honeyTokenId, options) => {
9564
9844
  const ctx = buildContext();
9565
9845
  const orgRef = await resolveOrg(ctx, options.org);
9566
9846
  await confirmDestructive(options.yes, `Delete ${honeyTokenId}? Wherever you planted it goes back to being unwatched — pull the bait too.`);
@@ -9584,7 +9864,7 @@ function collectHeader(value, acc = {}) {
9584
9864
  */
9585
9865
  function registerLogSinkCommands(program) {
9586
9866
  const sink = program.command("log-sink").description("stream the audit trail to your own OTLP collector (SIEM)");
9587
- sink.command("show", { isDefault: true }).description("show the configured log sink and its delivery health").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9867
+ sink.command("show", { isDefault: true }).description("show the configured log sink and its delivery health").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
9588
9868
  const ctx = buildContext();
9589
9869
  const ref = await resolveOrg(ctx, options.org);
9590
9870
  const { sink: config } = await ctx.client.getLogSink(ref.id);
@@ -9603,7 +9883,7 @@ function registerLogSinkCommands(program) {
9603
9883
  ]);
9604
9884
  });
9605
9885
  });
9606
- sink.command("set <endpoint>").description("point the audit export at an OTLP/HTTP logs endpoint").option("--org <slug>").option("--header <name: value>", "auth header to send (repeatable; values are write-only)", collectHeader).option("--clear-headers", "send no headers at all (drops the stored ones)").option("--disabled", "save the config but stop shipping").action(async (endpoint, options) => {
9886
+ sink.command("set <endpoint>").description("point the audit export at an OTLP/HTTP logs endpoint").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--header <name: value>", "auth header to send (repeatable; values are write-only)", collectHeader).option("--clear-headers", "send no headers at all (drops the stored ones)").option("--disabled", "save the config but stop shipping").action(async (endpoint, options) => {
9607
9887
  if (options.header && options.clearHeaders) fail("pass either --header or --clear-headers, not both");
9608
9888
  const ctx = buildContext();
9609
9889
  const ref = await resolveOrg(ctx, options.org);
@@ -9615,7 +9895,7 @@ function registerLogSinkCommands(program) {
9615
9895
  });
9616
9896
  console.error(`log sink → ${config.endpoint} (${config.enabled ? "enabled" : "disabled"}) — test it with \`seekrit log-sink test\``);
9617
9897
  });
9618
- sink.command("test").description("send a probe to the configured endpoint and report the result").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
9898
+ sink.command("test").description("send a probe to the configured endpoint and report the result").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
9619
9899
  const ctx = buildContext();
9620
9900
  const ref = await resolveOrg(ctx, options.org);
9621
9901
  const result = await ctx.client.testLogSink(ref.id);
@@ -9628,7 +9908,7 @@ function registerLogSinkCommands(program) {
9628
9908
  });
9629
9909
  if (!result.ok) process.exitCode = 1;
9630
9910
  });
9631
- sink.command("rm").alias("delete").description("stop exporting the audit trail and forget the endpoint").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
9911
+ sink.command("rm").alias("delete").description("stop exporting the audit trail and forget the endpoint").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
9632
9912
  const ctx = buildContext();
9633
9913
  const ref = await resolveOrg(ctx, options.org);
9634
9914
  await confirmDestructive(options.yes, `Remove ${ref.slug}'s audit log export?`);
@@ -9762,7 +10042,7 @@ function resolveAdminUri(uri) {
9762
10042
  function registerMongoCommands(program) {
9763
10043
  const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
9764
10044
  const target = mongo.command("target").description("manage MongoDB targets");
9765
- target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
10045
+ target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
9766
10046
  const ctx = buildContext();
9767
10047
  const org = await resolveOrg(ctx, options.org);
9768
10048
  const adminUri = resolveAdminUri(options.uri);
@@ -9793,7 +10073,7 @@ function registerMongoCommands(program) {
9793
10073
  console.error("\nEnsure a provisioning user exists, then `seekrit mongodb lease`:\n");
9794
10074
  console.log(mongoAdminSetupInstructions(config));
9795
10075
  });
9796
- target.command("list").description("list MongoDB targets").option("--org <slug>").action(async (options) => {
10076
+ target.command("list").description("list MongoDB targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9797
10077
  const ctx = buildContext();
9798
10078
  const org = await resolveOrg(ctx, options.org);
9799
10079
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9804,13 +10084,14 @@ function registerMongoCommands(program) {
9804
10084
  console.log(`${t.id}\t${t.name}\t${host}\t${cfg.connection.database}\t${cfg.accessLevel ?? "readonly"}`);
9805
10085
  }
9806
10086
  });
9807
- target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>").action(async (targetId, options) => {
10087
+ target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
9808
10088
  const ctx = buildContext();
9809
10089
  const org = await resolveOrg(ctx, options.org);
10090
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
9810
10091
  await ctx.client.deleteLeaseTarget(org.id, targetId);
9811
10092
  console.error(`deleted ${targetId}`);
9812
10093
  });
9813
- mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
10094
+ mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
9814
10095
  const ctx = buildContext();
9815
10096
  const org = await resolveOrg(ctx, options.org);
9816
10097
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9829,7 +10110,7 @@ function registerMongoCommands(program) {
9829
10110
  if (options.json) console.log(JSON.stringify(cred, null, 2));
9830
10111
  else console.log(`export MONGODB_URI='${cred.uri}'`);
9831
10112
  });
9832
- mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
10113
+ mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9833
10114
  const ctx = buildContext();
9834
10115
  const org = await resolveOrg(ctx, options.org);
9835
10116
  const { leases } = await ctx.client.listLeases(org.id);
@@ -9838,9 +10119,10 @@ function registerMongoCommands(program) {
9838
10119
  console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
9839
10120
  }
9840
10121
  });
9841
- mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>").action(async (leaseId, options) => {
10122
+ mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
9842
10123
  const ctx = buildContext();
9843
10124
  const org = await resolveOrg(ctx, options.org);
10125
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its MongoDB user is dropped immediately.`);
9844
10126
  await ctx.client.revokeLease(org.id, leaseId);
9845
10127
  console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
9846
10128
  });
@@ -9922,7 +10204,7 @@ function generateUserName$1(prefix = "tmp") {
9922
10204
  function registerMysqlCommands(program) {
9923
10205
  const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
9924
10206
  const target = mysql.command("target").description("manage provisioning targets");
9925
- target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
10207
+ target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
9926
10208
  const ctx = buildContext();
9927
10209
  const org = await resolveOrg(ctx, options.org);
9928
10210
  const executor = options.executor === "remote" ? "remote" : "in_do";
@@ -9964,7 +10246,7 @@ function registerMysqlCommands(program) {
9964
10246
  });
9965
10247
  console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
9966
10248
  });
9967
- target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
10249
+ target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
9968
10250
  const ctx = buildContext();
9969
10251
  const org = await resolveOrg(ctx, options.org);
9970
10252
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -9974,13 +10256,14 @@ function registerMysqlCommands(program) {
9974
10256
  console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
9975
10257
  }
9976
10258
  });
9977
- target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
10259
+ target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
9978
10260
  const ctx = buildContext();
9979
10261
  const org = await resolveOrg(ctx, options.org);
10262
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
9980
10263
  await ctx.client.deleteLeaseTarget(org.id, targetId);
9981
10264
  console.error(`removed ${targetId}`);
9982
10265
  });
9983
- mysql.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--user <name>", "user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
10266
+ mysql.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <name>", "user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
9984
10267
  const ctx = buildContext();
9985
10268
  const org = await resolveOrg(ctx, options.org);
9986
10269
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -10006,7 +10289,7 @@ function registerMysqlCommands(program) {
10006
10289
  }, null, 2));
10007
10290
  else console.log(url);
10008
10291
  });
10009
- mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
10292
+ mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
10010
10293
  const ctx = buildContext();
10011
10294
  const org = await resolveOrg(ctx, options.org);
10012
10295
  const { leases } = await ctx.client.listLeases(org.id);
@@ -10015,9 +10298,10 @@ function registerMysqlCommands(program) {
10015
10298
  console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
10016
10299
  }
10017
10300
  });
10018
- mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>").action(async (leaseId, options) => {
10301
+ mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
10019
10302
  const ctx = buildContext();
10020
10303
  const org = await resolveOrg(ctx, options.org);
10304
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its MySQL user is dropped immediately.`);
10021
10305
  await ctx.client.revokeLease(org.id, leaseId);
10022
10306
  console.error(`revoked ${leaseId}`);
10023
10307
  });
@@ -10027,106 +10311,695 @@ function collect$3(value, acc) {
10027
10311
  acc.push(value);
10028
10312
  return acc;
10029
10313
  }
10030
- //#endregion
10031
- //#region src/orgs.ts
10032
10314
  /**
10033
- * Counts for `org show`. Each list is admin-gated to a different degree, so a
10034
- * member who can't read one still gets the rest: a failed call reports as
10035
- * `null`, which `printFields` drops and JSON preserves as "not visible to you".
10315
+ * Fetch + decrypt every secret in a single environment.
10316
+ *
10317
+ * `${OTHER_SECRET}` references are expanded (see `@seekrit/core`'s
10318
+ * `interpolate`) unless `raw` is set. Only this environment's own secrets are in
10319
+ * scope here — a reference to a secret inherited from a composed group is left
10320
+ * literal, because the group layers aren't fetched. `materializeEnv` is the
10321
+ * fully-layered view.
10036
10322
  */
10037
- async function countOrNull(load) {
10323
+ async function fetchDecryptedSecrets(ctx, orgId, envId, opts = {}) {
10324
+ const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
10325
+ const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
10326
+ return interpolateValues(Object.fromEntries(entries), !opts.raw).values;
10327
+ }
10328
+ /**
10329
+ * Expand `${OTHER_SECRET}` references in a merged variable set. A cycle becomes
10330
+ * the CLI's standard fatal exit — it is a config bug with no correct value to
10331
+ * emit. Pass `enabled: false` (`--no-interpolate`) to hand the set back as-is.
10332
+ */
10333
+ function interpolateValues(values, enabled = true) {
10334
+ if (!enabled) return {
10335
+ values,
10336
+ interpolated: [],
10337
+ unresolvedRefs: []
10338
+ };
10038
10339
  try {
10039
- return await load();
10040
- } catch {
10041
- return null;
10340
+ const { values: expandedValues, expanded, unresolved } = interpolateSecrets(values);
10341
+ return {
10342
+ values: expandedValues,
10343
+ interpolated: expanded,
10344
+ unresolvedRefs: unresolved
10345
+ };
10346
+ } catch (err) {
10347
+ return fail(err instanceof Error ? err.message : String(err));
10042
10348
  }
10043
10349
  }
10044
- async function orgOverview(ctx, orgId) {
10045
- const [apps, groups, members, tokens] = await Promise.all([
10046
- countOrNull(async () => (await ctx.client.listApps(orgId)).apps.length),
10047
- countOrNull(async () => (await ctx.client.listGroups(orgId)).groups.length),
10048
- countOrNull(async () => (await ctx.client.listMembers(orgId)).members.length),
10049
- countOrNull(async () => (await ctx.client.listTokens(orgId)).tokens.length)
10050
- ]);
10350
+ /**
10351
+ * Decrypt one historical version of a secret. Ciphertext is bound to
10352
+ * `(envId, name)` as AAD and neither changes across versions, so an old blob
10353
+ * opens with the environment's current data key — no special handling needed.
10354
+ */
10355
+ async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
10356
+ const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
10357
+ const row = versions.find((v) => v.version === version);
10358
+ if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
10359
+ return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
10360
+ }
10361
+ async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
10362
+ const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
10363
+ await ctx.client.setSecret(orgId, envId, name, ciphertext);
10364
+ }
10365
+ /**
10366
+ * Encrypt and store many secrets into one environment. The DEK is fetched once
10367
+ * (so user auth prompts for the passphrase a single time, not per variable),
10368
+ * then each value is encrypted locally and written. Existing names are
10369
+ * overwritten; the result splits them into created vs. updated for a summary.
10370
+ * Callers validate the names first — a rejected name aborts before any write.
10371
+ */
10372
+ async function importSecrets(ctx, orgId, envId, entries) {
10373
+ const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
10374
+ const existing = new Set(secrets.map((s) => s.name));
10375
+ const result = {
10376
+ created: [],
10377
+ updated: []
10378
+ };
10379
+ for (const [name, value] of Object.entries(entries)) {
10380
+ const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
10381
+ await ctx.client.setSecret(orgId, envId, name, ciphertext);
10382
+ (existing.has(name) ? result.updated : result.created).push(name);
10383
+ }
10384
+ return result;
10385
+ }
10386
+ /**
10387
+ * Resolve the full, layered environment for a running app: composed group
10388
+ * secrets (lowest precedence) → the app env's own secrets → `.env` files.
10389
+ * Each layer's DEK is unwrapped once with the principal's private key and its
10390
+ * ciphertext decrypted locally. `process.env` is NOT applied here — callers
10391
+ * that spawn a process layer it on top so the live shell always wins.
10392
+ *
10393
+ * `${OTHER_SECRET}` references are expanded last, against the merged set, so a
10394
+ * reference always resolves to whichever layer won the name.
10395
+ */
10396
+ async function materializeEnv(ctx, opts) {
10397
+ const query = {};
10398
+ if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
10399
+ if (opts.branch) query.branch = opts.branch;
10400
+ if (!isTokenAuth(ctx)) {
10401
+ if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
10402
+ query.env = opts.envId;
10403
+ }
10404
+ const { scope, layers } = await resolveWithCache(ctx, query, opts.cache);
10405
+ const privateKey = await getPrivateKey(ctx);
10406
+ const values = {};
10407
+ const provenance = {};
10408
+ for (const layer of layers) {
10409
+ const dek = await unwrapDek(layer.wrappedDek, privateKey);
10410
+ let label;
10411
+ if (layer.source === "group") label = `group:${layer.groupSlug}@${layer.slug}`;
10412
+ else if (layer.source === "branch") label = `branch:${scope.appSlug}#${layer.slug}`;
10413
+ else label = `app:${scope.appSlug}/${layer.slug}`;
10414
+ for (const secret of layer.secrets) {
10415
+ values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
10416
+ provenance[secret.name] = label;
10417
+ }
10418
+ }
10419
+ const loadedEnvFiles = overlayEnvFiles(values, provenance, opts.envFiles);
10051
10420
  return {
10052
- apps,
10053
- groups,
10054
- members,
10055
- tokens
10421
+ ...interpolateValues(values, opts.interpolate !== false),
10422
+ provenance,
10423
+ scope,
10424
+ loadedEnvFiles
10056
10425
  };
10057
10426
  }
10058
- function registerOrgCommands(program) {
10059
- const org = program.command("org").description("manage organizations");
10060
- org.command("list").alias("ls").description("list the organizations you can access").option("--json", "print the raw API response").action(async (options) => {
10061
- const { orgs } = await buildContext().client.listOrgs();
10062
- emit(options, { orgs }, () => printTable(orgs, [
10063
- col("slug", (o) => o.slug),
10064
- col("name", (o) => o.name),
10065
- col("role", (o) => o.role),
10066
- col("id", (o) => o.id)
10067
- ], "no organizations create one with `seekrit org create`"));
10068
- });
10069
- org.command("show [slug]").description("show an organization and what it contains").option("--org <slug>", "organization slug (or pass it as the argument)").option("--json", "print the raw API response").action(async (slug, options) => {
10070
- const ctx = buildContext();
10071
- const ref = await resolveOrg(ctx, slug ?? options.org);
10072
- const { org: row } = await ctx.client.getOrg(ref.id);
10073
- const counts = await orgOverview(ctx, ref.id);
10074
- const shown = (n) => n === null ? "—" : n;
10075
- emit(options, {
10076
- org: row,
10077
- counts
10078
- }, () => {
10079
- printFields([
10080
- ["slug", row.slug],
10081
- ["name", row.name],
10082
- ["id", row.id],
10083
- ["your role", row.role],
10084
- ["created", row.createdAt],
10085
- ["applications", shown(counts.apps)],
10086
- ["groups", shown(counts.groups)],
10087
- ["members", shown(counts.members)],
10088
- ["service tokens", shown(counts.tokens)]
10089
- ]);
10090
- });
10091
- });
10092
- org.command("create").description("create an organization (you become its owner)").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
10093
- const created = await buildContext().client.createOrg({
10094
- name: options.name,
10095
- slug: options.slug
10096
- });
10097
- console.error(`created org ${created.org.slug} (${created.org.id})`);
10098
- });
10099
- org.command("rename").description("change an organization's display name (the slug is permanent)").option("--org <slug>").requiredOption("--name <name>", "new display name").action(async (options) => {
10100
- const ctx = buildContext();
10101
- const ref = await resolveOrg(ctx, options.org);
10102
- const { org: row } = await ctx.client.updateOrg(ref.id, { name: options.name });
10103
- console.error(`renamed ${row.slug} to "${row.name}"`);
10104
- });
10105
- org.command("member").description("view organization members").command("list").alias("ls").description("list members, and whether each has finished key setup").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
10106
- const ctx = buildContext();
10107
- const ref = await resolveOrg(ctx, options.org);
10108
- const { members } = await ctx.client.listMembers(ref.id);
10109
- emit(options, { members }, () => printTable(members, [
10110
- col("email", (m) => m.email),
10111
- col("role", (m) => m.role),
10112
- col("name", (m) => m.name),
10113
- col("keys", (m) => m.publicKeyJwk ? "ready" : "pending"),
10114
- col("id", (m) => m.userId)
10115
- ], "no members"));
10116
- });
10117
- const invite = org.command("invite").description("manage pending invitations");
10118
- invite.command("list").alias("ls").description("list outstanding invitations").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
10119
- const ctx = buildContext();
10120
- const ref = await resolveOrg(ctx, options.org);
10121
- const { invites } = await ctx.client.listInvites(ref.id);
10122
- emit(options, { invites }, () => printTable(invites, [
10123
- col("email", (i) => i.email),
10427
+ /**
10428
+ * Resolve, going through the last-known-good cache when one is configured.
10429
+ *
10430
+ * Always live first: the cache exists for when the call cannot land, not to
10431
+ * save a round trip, so a recovered network is picked up on the very next
10432
+ * invocation. A *refused* resolve (401/403/…) drops the entry rather than
10433
+ * falling back to it — otherwise revoking a token would keep working offline
10434
+ * until the entry aged out.
10435
+ */
10436
+ async function resolveWithCache(ctx, query, cache) {
10437
+ if (!cache) return ctx.client.resolve(query);
10438
+ try {
10439
+ const response = await ctx.client.resolve(query);
10440
+ try {
10441
+ cache.write(JSON.stringify(response));
10442
+ } catch (err) {
10443
+ warn(`could not update the cache: ${errorMessage(err)}`);
10444
+ }
10445
+ return response;
10446
+ } catch (err) {
10447
+ if (!mayFallBack(err)) {
10448
+ cache.invalidate();
10449
+ throw err;
10450
+ }
10451
+ const found = cache.read();
10452
+ if (found.kind === "hit") {
10453
+ warn(`${errorMessage(err)} — using cached secrets fetched ${humanize(found.ageMs)} ago`);
10454
+ return JSON.parse(found.body);
10455
+ }
10456
+ if (found.kind === "expired") warn(`cached secrets are ${humanize(found.ageMs)} old, past --cache-max-age`);
10457
+ else if (found.kind === "unusable") warn(`ignoring the cached secrets: ${found.reason}`);
10458
+ throw err;
10459
+ }
10460
+ }
10461
+ function warn(text) {
10462
+ process.stderr.write(`seekrit: ${text}\n`);
10463
+ }
10464
+ function errorMessage(err) {
10465
+ return err instanceof Error ? err.message : String(err);
10466
+ }
10467
+ /**
10468
+ * Overlay `.env` files onto an existing value/provenance set (later files win).
10469
+ * Missing files are skipped. Returns the files that were actually loaded. Used
10470
+ * both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
10471
+ * managed secrets are unavailable but `.env` should still apply.
10472
+ */
10473
+ function overlayEnvFiles(values, provenance, envFiles) {
10474
+ const loaded = [];
10475
+ for (const file of envFiles) {
10476
+ if (!existsSync(file)) continue;
10477
+ loaded.push(file);
10478
+ for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
10479
+ values[name] = value;
10480
+ provenance[name] = `dotenv:${file}`;
10481
+ }
10482
+ }
10483
+ return loaded;
10484
+ }
10485
+ /**
10486
+ * Print a name → source table to stderr (never the secret values). Names whose
10487
+ * value had references expanded are marked, and dangling references are called
10488
+ * out afterwards — a typo'd `${NAME}` is otherwise invisible, since it is
10489
+ * deliberately passed through as literal text.
10490
+ */
10491
+ function printExplain(provenance, refs = {}) {
10492
+ const interpolated = new Set(refs.interpolated ?? []);
10493
+ const names = Object.keys(provenance).sort();
10494
+ const width = names.reduce((w, n) => Math.max(w, n.length), 0);
10495
+ for (const name of names) {
10496
+ const marker = interpolated.has(name) ? " (interpolated)" : "";
10497
+ process.stderr.write(`${name.padEnd(width)} ${provenance[name]}${marker}\n`);
10498
+ }
10499
+ if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
10500
+ }
10501
+ /** Plugin id and integration id declared by `@seekrit/openclaw-plugin`. */
10502
+ const PLUGIN_ID = "seekrit";
10503
+ const PLUGIN_INTEGRATION_ID = "seekrit";
10504
+ /** The provider alias written into `secrets.providers`. */
10505
+ const PROVIDER_ALIAS = "seekrit";
10506
+ /**
10507
+ * Environment the resolver needs, and nothing else.
10508
+ *
10509
+ * OpenClaw hands an exec provider an empty environment apart from this
10510
+ * allowlist, which is a feature: it is the difference between "the resolver can
10511
+ * read the credential it needs" and "the resolver inherits the whole gateway
10512
+ * environment". `PATH` is here because Node's own startup needs it, `HOME` for
10513
+ * the CLI's login session, and the `SEEKRIT_*` set is how a machine credential
10514
+ * reaches a process nobody gets to pass flags to.
10515
+ */
10516
+ const PASS_ENV = [
10517
+ "PATH",
10518
+ "HOME",
10519
+ "USERPROFILE",
10520
+ "APPDATA",
10521
+ "LOCALAPPDATA",
10522
+ "TEMP",
10523
+ "TMP",
10524
+ "SYSTEMROOT",
10525
+ "WINDIR",
10526
+ "XDG_CONFIG_HOME",
10527
+ "XDG_CACHE_HOME",
10528
+ "NODE_EXTRA_CA_CERTS",
10529
+ "SEEKRIT_TOKEN",
10530
+ "SEEKRIT_CLIENT_ID",
10531
+ "SEEKRIT_CLIENT_SECRET",
10532
+ "SEEKRIT_API_URL",
10533
+ "SEEKRIT_ORG",
10534
+ "SEEKRIT_APP",
10535
+ "SEEKRIT_ENV",
10536
+ "SEEKRIT_BRANCH"
10537
+ ];
10538
+ /**
10539
+ * Generous, because the first resolve of a cold start does real work — an M2M
10540
+ * token mint, a resolve, and a key unwrap — and OpenClaw fails startup rather
10541
+ * than degrading when this expires. The 1Password integration picks 90s for the
10542
+ * same reason (its CLI may prompt for biometrics); 30s is enough here because
10543
+ * nothing in this path is interactive.
10544
+ */
10545
+ const TIMEOUT_MS = 3e4;
10546
+ /** A seekrit secret name: what `seekrit secrets set` accepts. */
10547
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
10548
+ /** Slugs as the API spells them. */
10549
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
10550
+ /**
10551
+ * Parse an exec SecretRef id.
10552
+ *
10553
+ * Two shapes, mirroring 1Password's `op://vault/item/field` — a bare name for
10554
+ * the environment the credential already points at, and an explicit
10555
+ * `app/env/NAME` triple for a provider serving more than one environment:
10556
+ *
10557
+ * OPENAI_API_KEY
10558
+ * billing-api/production/STRIPE_SECRET_KEY
10559
+ *
10560
+ * A bare name is the common case and the one a service token wants, since the
10561
+ * token is already bound to an environment and there is nothing to disambiguate.
10562
+ */
10563
+ function parseSecretId(id) {
10564
+ const trimmed = id.trim();
10565
+ if (trimmed.length === 0) return void 0;
10566
+ const parts = trimmed.split("/");
10567
+ if (parts.length === 1) {
10568
+ const [name = ""] = parts;
10569
+ return NAME_RE.test(name) ? { name } : void 0;
10570
+ }
10571
+ if (parts.length !== 3) return void 0;
10572
+ const [app = "", env = "", name = ""] = parts;
10573
+ if (!SLUG_RE.test(app) || !SLUG_RE.test(env) || !NAME_RE.test(name)) return void 0;
10574
+ return {
10575
+ app,
10576
+ env,
10577
+ name
10578
+ };
10579
+ }
10580
+ /** The (app, env) pair an id resolves against — `""` for the credential's own. */
10581
+ function scopeKey(id) {
10582
+ return id.app && id.env ? `${id.app}/${id.env}` : "";
10583
+ }
10584
+ /**
10585
+ * Parse a request, tolerantly in exactly one direction.
10586
+ *
10587
+ * Non-string and empty ids are dropped rather than rejected — a request whose
10588
+ * shape we do not recognize should still resolve the ids we do, because the
10589
+ * alternative fails OpenClaw's whole startup over one malformed entry. A body
10590
+ * that is not an object with an `ids` array is a different matter: there is
10591
+ * nothing to answer, so it throws.
10592
+ */
10593
+ function parseExecRequest(input) {
10594
+ const parsed = JSON.parse(input);
10595
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("exec SecretRef request must be a JSON object");
10596
+ const ids = parsed.ids;
10597
+ if (!Array.isArray(ids)) throw new Error("exec SecretRef request must carry an `ids` array");
10598
+ return { ids: ids.filter((id) => typeof id === "string" && id.trim().length > 0) };
10599
+ }
10600
+ /** Assemble a response, omitting `errors` entirely when everything resolved. */
10601
+ function buildExecResponse(values, errors) {
10602
+ return {
10603
+ protocolVersion: 1,
10604
+ values,
10605
+ ...Object.keys(errors).length > 0 ? { errors } : {}
10606
+ };
10607
+ }
10608
+ /**
10609
+ * Resolve every requested id, reading each distinct environment exactly once.
10610
+ *
10611
+ * A batch is normally one environment, but a provider may serve several, and
10612
+ * resolving per id would mean an unwrap per id. Grouping keeps a 40-variable
10613
+ * gateway to one round trip.
10614
+ *
10615
+ * `resolveScope` is injected so the protocol can be tested without a network,
10616
+ * an API, or a key.
10617
+ */
10618
+ async function resolveIds(ids, resolveScope) {
10619
+ const values = {};
10620
+ const errors = {};
10621
+ const byScope = /* @__PURE__ */ new Map();
10622
+ for (const id of ids) {
10623
+ const parsed = parseSecretId(id);
10624
+ if (!parsed) {
10625
+ errors[id] = { code: "INVALID_ID" };
10626
+ continue;
10627
+ }
10628
+ const key = scopeKey(parsed);
10629
+ const bucket = byScope.get(key) ?? {
10630
+ scope: parsed,
10631
+ ids: []
10632
+ };
10633
+ bucket.ids.push({
10634
+ id,
10635
+ name: parsed.name
10636
+ });
10637
+ byScope.set(key, bucket);
10638
+ }
10639
+ for (const { scope, ids: wanted } of byScope.values()) {
10640
+ let resolved;
10641
+ try {
10642
+ resolved = await resolveScope(scope);
10643
+ } catch (err) {
10644
+ const code = classify(err);
10645
+ for (const { id } of wanted) errors[id] = { code };
10646
+ continue;
10647
+ }
10648
+ for (const { id, name } of wanted) {
10649
+ const value = resolved[name];
10650
+ if (value === void 0) errors[id] = { code: "NOT_FOUND" };
10651
+ else values[id] = value;
10652
+ }
10653
+ }
10654
+ return buildExecResponse(values, errors);
10655
+ }
10656
+ /**
10657
+ * Bucket a failure into a code an operator can act on.
10658
+ *
10659
+ * Deliberately coarse and deliberately message-free: it reads the error's own
10660
+ * text to pick a bucket and then throws that text away, because the text may
10661
+ * quote a token.
10662
+ */
10663
+ function classify(err) {
10664
+ const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
10665
+ if (/unauthor|forbidden|token|credential|passphrase|decrypt|sign in|log in|login/.test(message)) return "UNAUTHORIZED";
10666
+ return "UNAVAILABLE";
10667
+ }
10668
+ /** OpenClaw's config directory: `$OPENCLAW_HOME`, else `~/.openclaw`. */
10669
+ function openclawHome(env = process.env) {
10670
+ const home = env.OPENCLAW_HOME?.trim();
10671
+ return home && home.length > 0 ? resolve(home) : join(homedir(), ".openclaw");
10672
+ }
10673
+ /**
10674
+ * The provider block for an installed `@seekrit/openclaw-plugin`.
10675
+ *
10676
+ * Preferred over the exec shape below for one reason that matters on upgrade:
10677
+ * OpenClaw reads the command out of the plugin's manifest at every startup, so
10678
+ * a plugin release that moves its resolver keeps working. A copied command is a
10679
+ * path pinned in someone's config forever.
10680
+ */
10681
+ function pluginProvider() {
10682
+ return {
10683
+ source: "exec",
10684
+ pluginIntegration: {
10685
+ pluginId: PLUGIN_ID,
10686
+ integrationId: PLUGIN_INTEGRATION_ID
10687
+ }
10688
+ };
10689
+ }
10690
+ /**
10691
+ * The provider block for a plain CLI install, with no plugin.
10692
+ *
10693
+ * `realpathSync` on both paths is not tidiness. OpenClaw's guard rejects a
10694
+ * symlinked command outright, and a Node installed by a version manager is
10695
+ * almost always reached through one — so resolving here is the difference
10696
+ * between a provider that works and a startup failure whose message points at
10697
+ * the config rather than at the shim.
10698
+ */
10699
+ function execProvider(cliEntry, nodeBinary = process.execPath) {
10700
+ const command = realTo(nodeBinary);
10701
+ const entry = realTo(cliEntry);
10702
+ return {
10703
+ source: "exec",
10704
+ command,
10705
+ args: [
10706
+ entry,
10707
+ "openclaw",
10708
+ "resolve"
10709
+ ],
10710
+ passEnv: [...PASS_ENV],
10711
+ jsonOnly: true,
10712
+ timeoutMs: TIMEOUT_MS,
10713
+ noOutputTimeoutMs: TIMEOUT_MS,
10714
+ trustedDirs: [.../* @__PURE__ */ new Set([dirname(command), dirname(entry)])]
10715
+ };
10716
+ }
10717
+ function realTo(path) {
10718
+ try {
10719
+ return realpathSync(path);
10720
+ } catch {
10721
+ return resolve(path);
10722
+ }
10723
+ }
10724
+ /** This CLI's own entry file — what `execProvider` hands to Node. */
10725
+ function cliEntryPath() {
10726
+ return fileURLToPath(new URL("index.js", import.meta.url));
10727
+ }
10728
+ /**
10729
+ * Add the seekrit provider to a parsed config without disturbing anything else.
10730
+ *
10731
+ * An existing `seekrit` entry that differs is left alone unless forced, for the
10732
+ * same reason `seekrit paperclip init` leaves an existing MCP server alone:
10733
+ * someone who pinned a timeout or narrowed `passEnv` did it deliberately, and
10734
+ * silently reverting a security narrowing is the worst kind of helpful.
10735
+ */
10736
+ function mergeProvider(existing, provider, force) {
10737
+ const secrets = { ...existing.secrets ?? {} };
10738
+ const providers = { ...secrets.providers ?? {} };
10739
+ const current = providers[PROVIDER_ALIAS];
10740
+ if (current && !force && JSON.stringify(current) !== JSON.stringify(provider)) return {
10741
+ merged: existing,
10742
+ changed: false
10743
+ };
10744
+ if (current && JSON.stringify(current) === JSON.stringify(provider)) return {
10745
+ merged: existing,
10746
+ changed: false
10747
+ };
10748
+ providers[PROVIDER_ALIAS] = provider;
10749
+ return {
10750
+ merged: {
10751
+ ...existing,
10752
+ secrets: {
10753
+ ...secrets,
10754
+ providers
10755
+ }
10756
+ },
10757
+ changed: true
10758
+ };
10759
+ }
10760
+ /** A SecretRef pointing at this provider — what replaces a plaintext key. */
10761
+ function secretRef(id) {
10762
+ return {
10763
+ source: "exec",
10764
+ provider: PROVIDER_ALIAS,
10765
+ id
10766
+ };
10767
+ }
10768
+ /**
10769
+ * Read `openclaw.json`, or decline to.
10770
+ *
10771
+ * The file is JSON5: comments and trailing commas are legal, and a parse/write
10772
+ * round trip through `JSON` would delete every comment in someone's gateway
10773
+ * config. So a file that is not also valid strict JSON is not edited at all —
10774
+ * the caller prints the block to paste instead. Losing a config's comments to
10775
+ * be helpful is worse than asking for one paste.
10776
+ */
10777
+ function readOpenclawConfig(path) {
10778
+ if (!existsSync(path)) return { config: {} };
10779
+ let raw;
10780
+ try {
10781
+ raw = readFileSync(path, "utf8");
10782
+ } catch (err) {
10783
+ fail(`could not read ${path}: ${err instanceof Error ? err.message : String(err)}`);
10784
+ }
10785
+ if (raw.trim().length === 0) return { config: {} };
10786
+ try {
10787
+ const parsed = JSON.parse(raw);
10788
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { json5: true };
10789
+ return { config: parsed };
10790
+ } catch {
10791
+ return { json5: true };
10792
+ }
10793
+ }
10794
+ function registerOpenclawCommands(program) {
10795
+ const openclaw = program.command("openclaw").description("wire seekrit into OpenClaw as a SecretRef provider (`seekrit openclaw --help`)");
10796
+ openclaw.command("init").description("add the seekrit exec SecretRef provider to openclaw.json").option("--config <path>", "path to openclaw.json (default: $OPENCLAW_HOME/openclaw.json)").option("--plugin", "point at an installed @seekrit/openclaw-plugin (recommended)").option("--exec", "point directly at this CLI, with no plugin installed").option("--write", "write the file instead of printing the block to paste").option("--force", "replace a seekrit provider entry that already differs").option("--json", "machine-readable output").action((options) => {
10797
+ if (options.plugin && options.exec) fail("--plugin and --exec are mutually exclusive");
10798
+ const usePlugin = options.exec !== true;
10799
+ let provider;
10800
+ if (usePlugin) provider = pluginProvider();
10801
+ else {
10802
+ const entry = cliEntryPath();
10803
+ if (!existsSync(entry)) fail(`cannot find this CLI's entry file at ${entry} — --exec needs an installed seekrit (npm i -g @seekrit/cli), or use --plugin`);
10804
+ provider = execProvider(entry);
10805
+ }
10806
+ const path = options.config ? resolve(options.config) : join(openclawHome(), "openclaw.json");
10807
+ const read = readOpenclawConfig(path);
10808
+ const json5 = "json5" in read;
10809
+ const merged = json5 ? void 0 : mergeProvider(read.config, provider, options.force === true);
10810
+ const wrote = Boolean(options.write) && merged?.changed === true;
10811
+ if (wrote && merged) writeFileSync(path, `${JSON.stringify(merged.merged, null, 2)}\n`, { mode: 384 });
10812
+ emit(options, {
10813
+ path,
10814
+ provider,
10815
+ wrote,
10816
+ json5
10817
+ }, () => {
10818
+ section("openclaw secret provider");
10819
+ printFields([
10820
+ ["config", path],
10821
+ ["provider", PROVIDER_ALIAS],
10822
+ ["shape", usePlugin ? "plugin integration" : "exec (this CLI)"],
10823
+ ["written", wrote ? "yes" : "no"]
10824
+ ]);
10825
+ console.log();
10826
+ if (json5) {
10827
+ console.log(`${path} is JSON5 (comments or trailing commas), which a rewrite here would delete.`);
10828
+ console.log("Merge this into it by hand:");
10829
+ } else if (!options.write) console.log("Merge this into your config, or re-run with --write:");
10830
+ else if (!wrote) console.log(`A different \`${PROVIDER_ALIAS}\` provider is already configured — left as it is. Re-run with --force to replace it.`);
10831
+ else console.log("Written. The block now in your config:");
10832
+ console.log();
10833
+ console.log(JSON.stringify({ secrets: { providers: { [PROVIDER_ALIAS]: provider } } }, null, 2));
10834
+ console.log();
10835
+ if (usePlugin) {
10836
+ console.log("Install the plugin, if you have not already:");
10837
+ console.log(" openclaw plugins install npm:@seekrit/openclaw-plugin");
10838
+ console.log(" openclaw plugins enable seekrit");
10839
+ console.log();
10840
+ }
10841
+ console.log("Then replace a plaintext credential with a ref, e.g.:");
10842
+ console.log(` ${JSON.stringify(secretRef("OPENAI_API_KEY"))}`);
10843
+ console.log();
10844
+ console.log("And check your work:");
10845
+ console.log(" openclaw secrets audit --check --allow-exec");
10846
+ console.log();
10847
+ console.log("Resolution is eager: OpenClaw reads every ref once at startup. After a rotation, run `openclaw secrets reload`.");
10848
+ });
10849
+ });
10850
+ openclaw.command("ref <name>").description("print the SecretRef object to paste in place of a plaintext credential").option("--app <slug>", "address a specific application (with --env)").option("--env <slug>", "address a specific environment (with --app)").option("--json", "machine-readable output").action((name, options) => {
10851
+ if (Boolean(options.app) !== Boolean(options.env)) fail("--app and --env go together (an id names both, or neither)");
10852
+ const id = options.app && options.env ? `${options.app}/${options.env}/${name}` : name;
10853
+ if (!parseSecretId(id)) fail(`not a valid SecretRef id: "${id}" (names are [A-Za-z_][A-Za-z0-9_]*)`);
10854
+ const ref = secretRef(id);
10855
+ emit(options, ref, () => console.log(JSON.stringify(ref)));
10856
+ });
10857
+ openclaw.command("resolve").description("resolve exec SecretRef ids from stdin (OpenClaw calls this; not for humans)").action(async () => {
10858
+ setFailThrows(true);
10859
+ let request;
10860
+ try {
10861
+ request = parseExecRequest(await readStdin());
10862
+ } catch (err) {
10863
+ process.stderr.write(`seekrit: ${err instanceof Error ? err.message : String(err)}\n`);
10864
+ process.exitCode = 2;
10865
+ return;
10866
+ }
10867
+ const scopes = /* @__PURE__ */ new Map();
10868
+ let ctx;
10869
+ const response = await resolveIds(request.ids, (scope) => {
10870
+ const key = scopeKey(scope);
10871
+ const cached = scopes.get(key);
10872
+ if (cached) return cached;
10873
+ ctx ??= buildContext();
10874
+ const pending = resolveEnvironment(ctx, scope);
10875
+ scopes.set(key, pending);
10876
+ return pending;
10877
+ });
10878
+ process.stdout.write(`${JSON.stringify(response)}\n`);
10879
+ });
10880
+ }
10881
+ /**
10882
+ * Resolve one environment's full variable set.
10883
+ *
10884
+ * `.env` overlays are deliberately switched off (`envFiles: []`). Everywhere
10885
+ * else in this CLI they are a convenience for a developer's shell; here the
10886
+ * working directory belongs to the OpenClaw gateway, and letting a file in it
10887
+ * override a gateway credential would make a stray `.env` a privilege
10888
+ * escalation. Reference expansion stays on — a `${OTHER_SECRET}` reference is
10889
+ * stored data, not a local override.
10890
+ */
10891
+ async function resolveEnvironment(ctx, scope) {
10892
+ let envId;
10893
+ if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, {
10894
+ app: scope.app,
10895
+ env: scope.env
10896
+ })).envId;
10897
+ const { values } = await materializeEnv(ctx, {
10898
+ envId,
10899
+ envFiles: []
10900
+ });
10901
+ return values;
10902
+ }
10903
+ //#endregion
10904
+ //#region src/orgs.ts
10905
+ /**
10906
+ * Counts for `org show`. Each list is admin-gated to a different degree, so a
10907
+ * member who can't read one still gets the rest: a failed call reports as
10908
+ * `null`, which `printFields` drops and JSON preserves as "not visible to you".
10909
+ */
10910
+ async function countOrNull(load) {
10911
+ try {
10912
+ return await load();
10913
+ } catch {
10914
+ return null;
10915
+ }
10916
+ }
10917
+ async function orgOverview(ctx, orgId) {
10918
+ const [apps, groups, members, tokens] = await Promise.all([
10919
+ countOrNull(async () => (await ctx.client.listApps(orgId)).apps.length),
10920
+ countOrNull(async () => (await ctx.client.listGroups(orgId)).groups.length),
10921
+ countOrNull(async () => (await ctx.client.listMembers(orgId)).members.length),
10922
+ countOrNull(async () => (await ctx.client.listTokens(orgId)).tokens.length)
10923
+ ]);
10924
+ return {
10925
+ apps,
10926
+ groups,
10927
+ members,
10928
+ tokens
10929
+ };
10930
+ }
10931
+ function registerOrgCommands(program) {
10932
+ const org = program.command("org").description("manage organizations");
10933
+ org.command("list").alias("ls").description("list the organizations you can access").option("--json", "print the raw API response").action(async (options) => {
10934
+ const { orgs } = await buildContext().client.listOrgs();
10935
+ emit(options, { orgs }, () => printTable(orgs, [
10936
+ col("slug", (o) => o.slug),
10937
+ col("name", (o) => o.name),
10938
+ col("role", (o) => o.role),
10939
+ col("id", (o) => o.id)
10940
+ ], "no organizations — create one with `seekrit org create`"));
10941
+ });
10942
+ org.command("show [slug]").description("show an organization and what it contains").option("--org <slug>", "organization slug (or pass it as the argument)").option("--json", "print the raw API response").action(async (slug, options) => {
10943
+ const ctx = buildContext();
10944
+ const ref = await resolveOrg(ctx, slug ?? options.org);
10945
+ const { org: row } = await ctx.client.getOrg(ref.id);
10946
+ const counts = await orgOverview(ctx, ref.id);
10947
+ const shown = (n) => n === null ? "—" : n;
10948
+ emit(options, {
10949
+ org: row,
10950
+ counts
10951
+ }, () => {
10952
+ printFields([
10953
+ ["slug", row.slug],
10954
+ ["name", row.name],
10955
+ ["id", row.id],
10956
+ ["your role", row.role],
10957
+ ["created", row.createdAt],
10958
+ ["applications", shown(counts.apps)],
10959
+ ["groups", shown(counts.groups)],
10960
+ ["members", shown(counts.members)],
10961
+ ["service tokens", shown(counts.tokens)]
10962
+ ]);
10963
+ });
10964
+ });
10965
+ org.command("create").description("create an organization (you become its owner)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
10966
+ const created = await buildContext().client.createOrg({
10967
+ name: options.name,
10968
+ slug: options.slug
10969
+ });
10970
+ console.error(`created org ${created.org.slug} (${created.org.id})`);
10971
+ });
10972
+ org.command("rename").description("change an organization's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "new display name").action(async (options) => {
10973
+ const ctx = buildContext();
10974
+ const ref = await resolveOrg(ctx, options.org);
10975
+ const { org: row } = await ctx.client.updateOrg(ref.id, { name: options.name });
10976
+ console.error(`renamed ${row.slug} to "${row.name}"`);
10977
+ });
10978
+ org.command("member").description("view organization members").command("list").alias("ls").description("list members, and whether each has finished key setup").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
10979
+ const ctx = buildContext();
10980
+ const ref = await resolveOrg(ctx, options.org);
10981
+ const { members } = await ctx.client.listMembers(ref.id);
10982
+ emit(options, { members }, () => printTable(members, [
10983
+ col("email", (m) => m.email),
10984
+ col("role", (m) => m.role),
10985
+ col("name", (m) => m.name),
10986
+ col("keys", (m) => m.publicKeyJwk ? "ready" : "pending"),
10987
+ col("id", (m) => m.userId)
10988
+ ], "no members"));
10989
+ });
10990
+ const invite = org.command("invite").description("manage pending invitations");
10991
+ invite.command("list").alias("ls").description("list outstanding invitations").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
10992
+ const ctx = buildContext();
10993
+ const ref = await resolveOrg(ctx, options.org);
10994
+ const { invites } = await ctx.client.listInvites(ref.id);
10995
+ emit(options, { invites }, () => printTable(invites, [
10996
+ col("email", (i) => i.email),
10124
10997
  col("role", (i) => i.role),
10125
10998
  col("invited", (i) => i.createdAt),
10126
10999
  col("id", (i) => i.id)
10127
11000
  ], "no pending invitations"));
10128
11001
  });
10129
- invite.command("add <email>").description("invite someone to the organization").option("--org <slug>").option("--role <role>", "admin | member", "member").action(async (email, options) => {
11002
+ invite.command("add <email>").description("invite someone to the organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--role <role>", "admin | member", "member").action(async (email, options) => {
10130
11003
  if (options.role !== "admin" && options.role !== "member") fail("--role must be admin or member");
10131
11004
  const ctx = buildContext();
10132
11005
  const ref = await resolveOrg(ctx, options.org);
@@ -10136,13 +11009,13 @@ function registerOrgCommands(program) {
10136
11009
  });
10137
11010
  console.error(`invited ${row.email} as ${row.role} (${row.id}) — they join when they first sign in`);
10138
11011
  });
10139
- invite.command("rm <inviteId>").description("rescind an invitation").option("--org <slug>").action(async (inviteId, options) => {
11012
+ invite.command("rm <inviteId>").description("rescind an invitation").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (inviteId, options) => {
10140
11013
  const ctx = buildContext();
10141
11014
  const ref = await resolveOrg(ctx, options.org);
10142
11015
  await ctx.client.revokeInvite(ref.id, inviteId);
10143
11016
  console.error(`${inviteId} revoked`);
10144
11017
  });
10145
- org.command("mfa").description("show or set the org-wide second-factor requirement").option("--org <slug>").option("--set <policy>", "required | optional").option("--json", "print the raw API response").action(async (options) => {
11018
+ org.command("mfa").description("show or set the org-wide second-factor requirement").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--set <policy>", "required | optional").option("--json", "print the raw API response").action(async (options) => {
10146
11019
  const ctx = buildContext();
10147
11020
  const ref = await resolveOrg(ctx, options.org);
10148
11021
  if (options.set !== void 0 && options.set !== "required" && options.set !== "optional") fail("--set must be required or optional");
@@ -10155,7 +11028,7 @@ function registerOrgCommands(program) {
10155
11028
  console.log(policy.required ? "required for all members" : "optional");
10156
11029
  });
10157
11030
  });
10158
- org.command("tree").description("print the org's applications, environments, and groups as a tree").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
11031
+ org.command("tree").description("print the org's applications, environments, and groups as a tree").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
10159
11032
  const ctx = buildContext();
10160
11033
  const ref = await resolveOrg(ctx, options.org);
10161
11034
  const [{ apps }, { groups }] = await Promise.all([ctx.client.listApps(ref.id), ctx.client.listGroups(ref.id)]);
@@ -11021,7 +11894,7 @@ function generateRoleName(prefix = "tmp") {
11021
11894
  function registerPgCommands(program) {
11022
11895
  const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
11023
11896
  const target = pg.command("target").description("manage provisioning targets");
11024
- target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$2, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$2, []).action(async (options) => {
11897
+ target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$2, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$2, []).action(async (options) => {
11025
11898
  const ctx = buildContext();
11026
11899
  const org = await resolveOrg(ctx, options.org);
11027
11900
  const executor = options.executor === "remote" ? "remote" : "in_do";
@@ -11067,7 +11940,7 @@ function registerPgCommands(program) {
11067
11940
  console.log(bootstrap);
11068
11941
  }
11069
11942
  });
11070
- target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
11943
+ target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
11071
11944
  const ctx = buildContext();
11072
11945
  const org = await resolveOrg(ctx, options.org);
11073
11946
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -11077,7 +11950,7 @@ function registerPgCommands(program) {
11077
11950
  console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
11078
11951
  }
11079
11952
  });
11080
- target.command("setup-sql <targetId>").description("print the one-time group-role setup SQL for a preset target").option("--org <slug>").action(async (targetId, options) => {
11953
+ target.command("setup-sql <targetId>").description("print the one-time group-role setup SQL for a preset target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
11081
11954
  const ctx = buildContext();
11082
11955
  const org = await resolveOrg(ctx, options.org);
11083
11956
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -11089,13 +11962,14 @@ function registerPgCommands(program) {
11089
11962
  if (!bootstrap) fail("this is a custom target — it has no generated setup SQL");
11090
11963
  console.log(bootstrap);
11091
11964
  });
11092
- target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
11965
+ target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
11093
11966
  const ctx = buildContext();
11094
11967
  const org = await resolveOrg(ctx, options.org);
11968
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
11095
11969
  await ctx.client.deleteLeaseTarget(org.id, targetId);
11096
11970
  console.error(`removed ${targetId}`);
11097
11971
  });
11098
- pg.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--role <name>", "role name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
11972
+ pg.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--role <name>", "role name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
11099
11973
  const ctx = buildContext();
11100
11974
  const org = await resolveOrg(ctx, options.org);
11101
11975
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -11121,15 +11995,16 @@ function registerPgCommands(program) {
11121
11995
  }, null, 2));
11122
11996
  else console.log(url);
11123
11997
  });
11124
- pg.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
11998
+ pg.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
11125
11999
  const ctx = buildContext();
11126
12000
  const org = await resolveOrg(ctx, options.org);
11127
12001
  const { leases } = await ctx.client.listLeases(org.id);
11128
12002
  for (const l of leases) console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
11129
12003
  });
11130
- pg.command("revoke <leaseId>").description("revoke a lease now (drops the role immediately)").option("--org <slug>").action(async (leaseId, options) => {
12004
+ pg.command("revoke <leaseId>").description("revoke a lease now (drops the role immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
11131
12005
  const ctx = buildContext();
11132
12006
  const org = await resolveOrg(ctx, options.org);
12007
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its Postgres role is dropped immediately.`);
11133
12008
  await ctx.client.revokeLease(org.id, leaseId);
11134
12009
  console.error(`revoked ${leaseId}`);
11135
12010
  });
@@ -11626,7 +12501,7 @@ function generateUserName(prefix = "tmp") {
11626
12501
  function registerRedisCommands(program) {
11627
12502
  const redis = program.command("redis").description("temporary Redis credentials (short-lived, zero-knowledge)");
11628
12503
  const target = redis.command("target").description("manage provisioning targets");
11629
- target.command("add").description("register a Redis server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-cache").option("--org <slug>").requiredOption("--host <host>", "redis host").option("--port <port>", "redis port", "6379").option("--db <index>", "logical database index (the /<n> in the URL)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin redis:// (or rediss://) connection string (or set SEEKRIT_REDIS_ADMIN_URL); wrapped locally").option("--create-statement <cmd>", "custom SETUSER template (repeatable)", collect$1, []).option("--revoke-statement <cmd>", "custom DELUSER template (repeatable)", collect$1, []).action(async (options) => {
12504
+ target.command("add").description("register a Redis server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-cache").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "redis host").option("--port <port>", "redis port", "6379").option("--db <index>", "logical database index (the /<n> in the URL)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin redis:// (or rediss://) connection string (or set SEEKRIT_REDIS_ADMIN_URL); wrapped locally").option("--create-statement <cmd>", "custom SETUSER template (repeatable)", collect$1, []).option("--revoke-statement <cmd>", "custom DELUSER template (repeatable)", collect$1, []).action(async (options) => {
11630
12505
  const ctx = buildContext();
11631
12506
  const org = await resolveOrg(ctx, options.org);
11632
12507
  const executor = options.executor === "remote" ? "remote" : "in_do";
@@ -11667,7 +12542,7 @@ function registerRedisCommands(program) {
11667
12542
  });
11668
12543
  console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
11669
12544
  });
11670
- target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
12545
+ target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
11671
12546
  const ctx = buildContext();
11672
12547
  const org = await resolveOrg(ctx, options.org);
11673
12548
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -11678,13 +12553,14 @@ function registerRedisCommands(program) {
11678
12553
  console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${db}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
11679
12554
  }
11680
12555
  });
11681
- target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
12556
+ target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
11682
12557
  const ctx = buildContext();
11683
12558
  const org = await resolveOrg(ctx, options.org);
12559
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
11684
12560
  await ctx.client.deleteLeaseTarget(org.id, targetId);
11685
12561
  console.error(`removed ${targetId}`);
11686
12562
  });
11687
- redis.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--user <name>", "ACL user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
12563
+ redis.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <name>", "ACL user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
11688
12564
  const ctx = buildContext();
11689
12565
  const org = await resolveOrg(ctx, options.org);
11690
12566
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -11710,7 +12586,7 @@ function registerRedisCommands(program) {
11710
12586
  }, null, 2));
11711
12587
  else console.log(url);
11712
12588
  });
11713
- redis.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
12589
+ redis.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
11714
12590
  const ctx = buildContext();
11715
12591
  const org = await resolveOrg(ctx, options.org);
11716
12592
  const { leases } = await ctx.client.listLeases(org.id);
@@ -11719,9 +12595,10 @@ function registerRedisCommands(program) {
11719
12595
  console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
11720
12596
  }
11721
12597
  });
11722
- redis.command("revoke <leaseId>").description("revoke a lease now (deletes the ACL user immediately)").option("--org <slug>").action(async (leaseId, options) => {
12598
+ redis.command("revoke <leaseId>").description("revoke a lease now (deletes the ACL user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
11723
12599
  const ctx = buildContext();
11724
12600
  const org = await resolveOrg(ctx, options.org);
12601
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its Redis ACL user is deleted immediately.`);
11725
12602
  await ctx.client.revokeLease(org.id, leaseId);
11726
12603
  console.error(`revoked ${leaseId}`);
11727
12604
  });
@@ -11813,7 +12690,7 @@ function buildConfig$1(kind, options) {
11813
12690
  }
11814
12691
  function registerRotationCommands(program) {
11815
12692
  const rotation = program.command("rotation").description("managed rotation of stored secret values (scheduled, zero-knowledge)");
11816
- rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>").option("--app <slug>").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
12693
+ rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>", "environment slug").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
11817
12694
  const ctx = buildContext();
11818
12695
  const target = await resolveEnvTarget(ctx, options);
11819
12696
  const config = buildConfig$1(options.kind, options);
@@ -11843,7 +12720,7 @@ function registerRotationCommands(program) {
11843
12720
  else console.error(`first rotation: ${created.nextRotateAt}`);
11844
12721
  console.log(created.id);
11845
12722
  });
11846
- rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>").option("--json", "print the full policies as JSON").action(async (options) => {
12723
+ rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the full policies as JSON").action(async (options) => {
11847
12724
  const ctx = buildContext();
11848
12725
  const org = await resolveOrg(ctx, options.org);
11849
12726
  const { rotations } = await ctx.client.listRotations(org.id);
@@ -11857,234 +12734,48 @@ function registerRotationCommands(program) {
11857
12734
  }
11858
12735
  for (const r of rotations) console.log(rotationLine(r));
11859
12736
  });
11860
- rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>").action(async (ref, options) => {
12737
+ rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
11861
12738
  const ctx = buildContext();
11862
12739
  const r = await resolveRotation(ctx, (await resolveOrg(ctx, options.org)).id, ref);
11863
12740
  console.log(JSON.stringify(r, null, 2));
11864
12741
  });
11865
- rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>").action(async (ref, options) => {
12742
+ rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
11866
12743
  const ctx = buildContext();
11867
12744
  const org = await resolveOrg(ctx, options.org);
11868
12745
  const r = await resolveRotation(ctx, org.id, ref);
11869
12746
  const { version, rotatedAt } = await ctx.client.rotateSecretNow(org.id, r.id);
11870
12747
  console.error(`rotated ${r.secretName} at ${rotatedAt} — now at version ${version}. Read it with \`seekrit secrets get ${r.secretName}\`.`);
11871
12748
  });
11872
- rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>").action(async (ref, options) => {
12749
+ rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
11873
12750
  const ctx = buildContext();
11874
12751
  const org = await resolveOrg(ctx, options.org);
11875
12752
  const r = await resolveRotation(ctx, org.id, ref);
11876
12753
  await ctx.client.updateRotation(org.id, r.id, { status: "paused" });
11877
12754
  console.error(`paused rotation of ${r.secretName}`);
11878
12755
  });
11879
- rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>").action(async (ref, options) => {
12756
+ rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
11880
12757
  const ctx = buildContext();
11881
12758
  const org = await resolveOrg(ctx, options.org);
11882
12759
  const r = await resolveRotation(ctx, org.id, ref);
11883
12760
  const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { status: "active" });
11884
12761
  console.error(`resumed rotation of ${r.secretName} — next ${updated.nextRotateAt}`);
11885
12762
  });
11886
- rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>").action(async (ref, options) => {
12763
+ rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
11887
12764
  const ctx = buildContext();
11888
12765
  const org = await resolveOrg(ctx, options.org);
11889
12766
  const r = await resolveRotation(ctx, org.id, ref);
11890
12767
  const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { intervalSeconds: parseDurationSeconds(options.every, "--every") });
11891
12768
  console.error(`${updated.secretName} now rotates every ${formatInterval(updated.intervalSeconds)} — next ${updated.nextRotateAt}`);
11892
12769
  });
11893
- rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>").action(async (ref, options) => {
12770
+ rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (ref, options) => {
11894
12771
  const ctx = buildContext();
11895
12772
  const org = await resolveOrg(ctx, options.org);
11896
12773
  const r = await resolveRotation(ctx, org.id, ref);
12774
+ await confirmDestructive(options.yes, `Stop rotating ${r.secretName} and delete its policy? The current value stays as it is.`);
11897
12775
  const { rotatorRevoked } = await ctx.client.disableRotation(org.id, r.id);
11898
12776
  console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
11899
12777
  });
11900
12778
  }
11901
- /**
11902
- * Fetch + decrypt every secret in a single environment.
11903
- *
11904
- * `${OTHER_SECRET}` references are expanded (see `@seekrit/core`'s
11905
- * `interpolate`) unless `raw` is set. Only this environment's own secrets are in
11906
- * scope here — a reference to a secret inherited from a composed group is left
11907
- * literal, because the group layers aren't fetched. `materializeEnv` is the
11908
- * fully-layered view.
11909
- */
11910
- async function fetchDecryptedSecrets(ctx, orgId, envId, opts = {}) {
11911
- const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
11912
- const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
11913
- return interpolateValues(Object.fromEntries(entries), !opts.raw).values;
11914
- }
11915
- /**
11916
- * Expand `${OTHER_SECRET}` references in a merged variable set. A cycle becomes
11917
- * the CLI's standard fatal exit — it is a config bug with no correct value to
11918
- * emit. Pass `enabled: false` (`--no-interpolate`) to hand the set back as-is.
11919
- */
11920
- function interpolateValues(values, enabled = true) {
11921
- if (!enabled) return {
11922
- values,
11923
- interpolated: [],
11924
- unresolvedRefs: []
11925
- };
11926
- try {
11927
- const { values: expandedValues, expanded, unresolved } = interpolateSecrets(values);
11928
- return {
11929
- values: expandedValues,
11930
- interpolated: expanded,
11931
- unresolvedRefs: unresolved
11932
- };
11933
- } catch (err) {
11934
- return fail(err instanceof Error ? err.message : String(err));
11935
- }
11936
- }
11937
- /**
11938
- * Decrypt one historical version of a secret. Ciphertext is bound to
11939
- * `(envId, name)` as AAD and neither changes across versions, so an old blob
11940
- * opens with the environment's current data key — no special handling needed.
11941
- */
11942
- async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
11943
- const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
11944
- const row = versions.find((v) => v.version === version);
11945
- if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
11946
- return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
11947
- }
11948
- async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
11949
- const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
11950
- await ctx.client.setSecret(orgId, envId, name, ciphertext);
11951
- }
11952
- /**
11953
- * Encrypt and store many secrets into one environment. The DEK is fetched once
11954
- * (so user auth prompts for the passphrase a single time, not per variable),
11955
- * then each value is encrypted locally and written. Existing names are
11956
- * overwritten; the result splits them into created vs. updated for a summary.
11957
- * Callers validate the names first — a rejected name aborts before any write.
11958
- */
11959
- async function importSecrets(ctx, orgId, envId, entries) {
11960
- const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
11961
- const existing = new Set(secrets.map((s) => s.name));
11962
- const result = {
11963
- created: [],
11964
- updated: []
11965
- };
11966
- for (const [name, value] of Object.entries(entries)) {
11967
- const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
11968
- await ctx.client.setSecret(orgId, envId, name, ciphertext);
11969
- (existing.has(name) ? result.updated : result.created).push(name);
11970
- }
11971
- return result;
11972
- }
11973
- /**
11974
- * Resolve the full, layered environment for a running app: composed group
11975
- * secrets (lowest precedence) → the app env's own secrets → `.env` files.
11976
- * Each layer's DEK is unwrapped once with the principal's private key and its
11977
- * ciphertext decrypted locally. `process.env` is NOT applied here — callers
11978
- * that spawn a process layer it on top so the live shell always wins.
11979
- *
11980
- * `${OTHER_SECRET}` references are expanded last, against the merged set, so a
11981
- * reference always resolves to whichever layer won the name.
11982
- */
11983
- async function materializeEnv(ctx, opts) {
11984
- const query = {};
11985
- if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
11986
- if (opts.branch) query.branch = opts.branch;
11987
- if (!isTokenAuth(ctx)) {
11988
- if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
11989
- query.env = opts.envId;
11990
- }
11991
- const { scope, layers } = await resolveWithCache(ctx, query, opts.cache);
11992
- const privateKey = await getPrivateKey(ctx);
11993
- const values = {};
11994
- const provenance = {};
11995
- for (const layer of layers) {
11996
- const dek = await unwrapDek(layer.wrappedDek, privateKey);
11997
- let label;
11998
- if (layer.source === "group") label = `group:${layer.groupSlug}@${layer.slug}`;
11999
- else if (layer.source === "branch") label = `branch:${scope.appSlug}#${layer.slug}`;
12000
- else label = `app:${scope.appSlug}/${layer.slug}`;
12001
- for (const secret of layer.secrets) {
12002
- values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
12003
- provenance[secret.name] = label;
12004
- }
12005
- }
12006
- const loadedEnvFiles = overlayEnvFiles(values, provenance, opts.envFiles);
12007
- return {
12008
- ...interpolateValues(values, opts.interpolate !== false),
12009
- provenance,
12010
- scope,
12011
- loadedEnvFiles
12012
- };
12013
- }
12014
- /**
12015
- * Resolve, going through the last-known-good cache when one is configured.
12016
- *
12017
- * Always live first: the cache exists for when the call cannot land, not to
12018
- * save a round trip, so a recovered network is picked up on the very next
12019
- * invocation. A *refused* resolve (401/403/…) drops the entry rather than
12020
- * falling back to it — otherwise revoking a token would keep working offline
12021
- * until the entry aged out.
12022
- */
12023
- async function resolveWithCache(ctx, query, cache) {
12024
- if (!cache) return ctx.client.resolve(query);
12025
- try {
12026
- const response = await ctx.client.resolve(query);
12027
- try {
12028
- cache.write(JSON.stringify(response));
12029
- } catch (err) {
12030
- warn(`could not update the cache: ${errorMessage(err)}`);
12031
- }
12032
- return response;
12033
- } catch (err) {
12034
- if (!mayFallBack(err)) {
12035
- cache.invalidate();
12036
- throw err;
12037
- }
12038
- const found = cache.read();
12039
- if (found.kind === "hit") {
12040
- warn(`${errorMessage(err)} — using cached secrets fetched ${humanize(found.ageMs)} ago`);
12041
- return JSON.parse(found.body);
12042
- }
12043
- if (found.kind === "expired") warn(`cached secrets are ${humanize(found.ageMs)} old, past --cache-max-age`);
12044
- else if (found.kind === "unusable") warn(`ignoring the cached secrets: ${found.reason}`);
12045
- throw err;
12046
- }
12047
- }
12048
- function warn(text) {
12049
- process.stderr.write(`seekrit: ${text}\n`);
12050
- }
12051
- function errorMessage(err) {
12052
- return err instanceof Error ? err.message : String(err);
12053
- }
12054
- /**
12055
- * Overlay `.env` files onto an existing value/provenance set (later files win).
12056
- * Missing files are skipped. Returns the files that were actually loaded. Used
12057
- * both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
12058
- * managed secrets are unavailable but `.env` should still apply.
12059
- */
12060
- function overlayEnvFiles(values, provenance, envFiles) {
12061
- const loaded = [];
12062
- for (const file of envFiles) {
12063
- if (!existsSync(file)) continue;
12064
- loaded.push(file);
12065
- for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
12066
- values[name] = value;
12067
- provenance[name] = `dotenv:${file}`;
12068
- }
12069
- }
12070
- return loaded;
12071
- }
12072
- /**
12073
- * Print a name → source table to stderr (never the secret values). Names whose
12074
- * value had references expanded are marked, and dangling references are called
12075
- * out afterwards — a typo'd `${NAME}` is otherwise invisible, since it is
12076
- * deliberately passed through as literal text.
12077
- */
12078
- function printExplain(provenance, refs = {}) {
12079
- const interpolated = new Set(refs.interpolated ?? []);
12080
- const names = Object.keys(provenance).sort();
12081
- const width = names.reduce((w, n) => Math.max(w, n.length), 0);
12082
- for (const name of names) {
12083
- const marker = interpolated.has(name) ? " (interpolated)" : "";
12084
- process.stderr.write(`${name.padEnd(width)} ${provenance[name]}${marker}\n`);
12085
- }
12086
- if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
12087
- }
12088
12779
  //#endregion
12089
12780
  //#region src/ssh.ts
12090
12781
  /**
@@ -12112,7 +12803,7 @@ function parseTtlSeconds(input) {
12112
12803
  function registerSshCommands(program) {
12113
12804
  const ssh = program.command("ssh").description("temporary SSH access (short-lived certificates, zero-knowledge)");
12114
12805
  const target = ssh.command("target").description("manage SSH CA targets");
12115
- target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
12806
+ target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
12116
12807
  const ctx = buildContext();
12117
12808
  const org = await resolveOrg(ctx, options.org);
12118
12809
  const ca = await generateSshCaKeyPair(`seekrit-ca:${options.name}`);
@@ -12139,7 +12830,7 @@ function registerSshCommands(program) {
12139
12830
  console.error("\nInstall the CA on your hosts, then issue certs with `seekrit ssh lease`:\n");
12140
12831
  console.log(sshHostSetupInstructions(config));
12141
12832
  });
12142
- target.command("list").description("list SSH CA targets").option("--org <slug>").action(async (options) => {
12833
+ target.command("list").description("list SSH CA targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
12143
12834
  const ctx = buildContext();
12144
12835
  const org = await resolveOrg(ctx, options.org);
12145
12836
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -12151,7 +12842,7 @@ function registerSshCommands(program) {
12151
12842
  console.log(`${t.id}\t${t.name}\t${where}\tprincipals=${principals}`);
12152
12843
  }
12153
12844
  });
12154
- target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>").action(async (targetId, options) => {
12845
+ target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
12155
12846
  const ctx = buildContext();
12156
12847
  const org = await resolveOrg(ctx, options.org);
12157
12848
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -12161,13 +12852,14 @@ function registerSshCommands(program) {
12161
12852
  if (cfg.provider !== "ssh") fail("not an ssh target (see `seekrit pg`)");
12162
12853
  console.log(sshHostSetupInstructions(cfg));
12163
12854
  });
12164
- target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>").action(async (targetId, options) => {
12855
+ target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
12165
12856
  const ctx = buildContext();
12166
12857
  const org = await resolveOrg(ctx, options.org);
12858
+ await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
12167
12859
  await ctx.client.deleteLeaseTarget(org.id, targetId);
12168
12860
  console.error(`deleted ${targetId}`);
12169
12861
  });
12170
- ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
12862
+ ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
12171
12863
  const ctx = buildContext();
12172
12864
  const org = await resolveOrg(ctx, options.org);
12173
12865
  const { targets } = await ctx.client.listLeaseTargets(org.id);
@@ -12205,7 +12897,7 @@ function registerSshCommands(program) {
12205
12897
  }, null, 2));
12206
12898
  else console.log(command);
12207
12899
  });
12208
- ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
12900
+ ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
12209
12901
  const ctx = buildContext();
12210
12902
  const org = await resolveOrg(ctx, options.org);
12211
12903
  const { leases } = await ctx.client.listLeases(org.id);
@@ -12214,9 +12906,10 @@ function registerSshCommands(program) {
12214
12906
  console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
12215
12907
  }
12216
12908
  });
12217
- ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>").action(async (leaseId, options) => {
12909
+ ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
12218
12910
  const ctx = buildContext();
12219
12911
  const org = await resolveOrg(ctx, options.org);
12912
+ await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Certificates already issued stay valid until they expire.`);
12220
12913
  await ctx.client.revokeLease(org.id, leaseId);
12221
12914
  console.error(`revoked ${leaseId} (issued certs remain valid until they expire)`);
12222
12915
  });
@@ -12732,7 +13425,7 @@ async function resolveConnection(ctx, orgId, ref) {
12732
13425
  }
12733
13426
  function registerSyncCommands(program) {
12734
13427
  const sync = program.command("sync").description("push environments to a third-party platform (Vercel, Cloudflare, …)");
12735
- sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
13428
+ sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
12736
13429
  const ctx = buildContext();
12737
13430
  const ref = await resolveOrg(ctx, options.org);
12738
13431
  const { connections } = await ctx.client.listSyncConnections(ref.id);
@@ -12744,7 +13437,7 @@ function registerSyncCommands(program) {
12744
13437
  col("id", (c) => c.id)
12745
13438
  ], "no connections — add one with `seekrit sync connect`"));
12746
13439
  });
12747
- sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--azure-tenant-id <id>", "azure-key-vault: Entra Directory (tenant) ID, a GUID").option("--azure-client-id <id>", "azure-key-vault: Application (client) ID of the service principal, a GUID (its client secret is read from stdin)").option("--azure-cloud <cloud>", `azure-key-vault: ${AZURE_CLOUDS.join(" | ")}`, "public").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com) · langgraph-platform: self-hosted LangSmith control-plane root").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").option("--langgraph-region <region>", `langgraph-platform: ${LANGGRAPH_PLATFORM_REGIONS.join(" | ")} (omit for us)`).option("--langgraph-tenant <id>", "langgraph-platform: LangSmith workspace UUID (only an org-scoped key needs it)").action(async (options) => {
13440
+ sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--azure-tenant-id <id>", "azure-key-vault: Entra Directory (tenant) ID, a GUID").option("--azure-client-id <id>", "azure-key-vault: Application (client) ID of the service principal, a GUID (its client secret is read from stdin)").option("--azure-cloud <cloud>", `azure-key-vault: ${AZURE_CLOUDS.join(" | ")}`, "public").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com) · langgraph-platform: self-hosted LangSmith control-plane root").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").option("--langgraph-region <region>", `langgraph-platform: ${LANGGRAPH_PLATFORM_REGIONS.join(" | ")} (omit for us)`).option("--langgraph-tenant <id>", "langgraph-platform: LangSmith workspace UUID (only an org-scoped key needs it)").action(async (options) => {
12748
13441
  const provider = assertProvider(options.provider);
12749
13442
  const ctx = buildContext();
12750
13443
  const ref = await resolveOrg(ctx, options.org);
@@ -12761,7 +13454,7 @@ function registerSyncCommands(program) {
12761
13454
  });
12762
13455
  console.error(`connected ${created.connection.name} (${created.connection.id})`);
12763
13456
  });
12764
- destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
13457
+ destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
12765
13458
  const provider = assertProvider(options.provider);
12766
13459
  const ctx = buildContext();
12767
13460
  const ref = await resolveOrg(ctx, options.org);
@@ -12770,7 +13463,7 @@ function registerSyncCommands(program) {
12770
13463
  emit(options, result, () => printFields([["result", result.ok ? "ok" : "failed"], ["error", result.error ?? null]]));
12771
13464
  if (!result.ok) process.exitCode = 1;
12772
13465
  });
12773
- sync.command("disconnect <connection>").description("delete a destination account, its bindings, and its keypair").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (connection, options) => {
13466
+ sync.command("disconnect <connection>").description("delete a destination account, its bindings, and its keypair").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (connection, options) => {
12774
13467
  const ctx = buildContext();
12775
13468
  const ref = await resolveOrg(ctx, options.org);
12776
13469
  const conn = await resolveConnection(ctx, ref.id, connection);
@@ -12780,7 +13473,7 @@ function registerSyncCommands(program) {
12780
13473
  await ctx.client.deleteSyncConnection(ref.id, conn.id);
12781
13474
  console.error(`disconnected ${conn.name}`);
12782
13475
  });
12783
- sync.command("bindings").description("list which environments are syncing where").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
13476
+ sync.command("bindings").description("list which environments are syncing where").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
12784
13477
  const ctx = buildContext();
12785
13478
  const ref = await resolveOrg(ctx, options.org);
12786
13479
  const [{ bindings }, { connections }] = await Promise.all([ctx.client.listSyncBindings(ref.id), ctx.client.listSyncConnections(ref.id)]);
@@ -12795,7 +13488,7 @@ function registerSyncCommands(program) {
12795
13488
  col("id", (b) => b.id)
12796
13489
  ], "nothing is syncing — enable it with `seekrit sync enable`"));
12797
13490
  });
12798
- destinationOptions(sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
13491
+ destinationOptions(sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
12799
13492
  const provider = assertProvider(options.provider);
12800
13493
  if (options.onDelete !== "delete" && options.onDelete !== "retain") fail("--on-delete must be delete or retain");
12801
13494
  if (options.mode !== "auto" && options.mode !== "manual") fail("--mode must be auto or manual");
@@ -12834,26 +13527,26 @@ function registerSyncCommands(program) {
12834
13527
  console.error(`syncing ${target.appSlug}/${target.envSlug} → ${conn.name} ${describeDestination(destination)} (${binding.id})`);
12835
13528
  if (binding.mode === "manual") console.error("mode is manual — push with `seekrit sync run`");
12836
13529
  });
12837
- sync.command("pause <bindingId>").description("stop pushing on this binding without deleting it").option("--org <slug>").action(async (bindingId, options) => {
13530
+ sync.command("pause <bindingId>").description("stop pushing on this binding without deleting it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (bindingId, options) => {
12838
13531
  const ctx = buildContext();
12839
13532
  const ref = await resolveOrg(ctx, options.org);
12840
13533
  await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: false });
12841
13534
  console.error(`${bindingId} paused`);
12842
13535
  });
12843
- sync.command("resume <bindingId>").description("start pushing on a paused binding again").option("--org <slug>").action(async (bindingId, options) => {
13536
+ sync.command("resume <bindingId>").description("start pushing on a paused binding again").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (bindingId, options) => {
12844
13537
  const ctx = buildContext();
12845
13538
  const ref = await resolveOrg(ctx, options.org);
12846
13539
  await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: true });
12847
13540
  console.error(`${bindingId} resumed`);
12848
13541
  });
12849
- sync.command("disable <bindingId>").alias("rm").description("stop syncing an environment and revoke seekrit's key for it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (bindingId, options) => {
13542
+ sync.command("disable <bindingId>").alias("rm").description("stop syncing an environment and revoke seekrit's key for it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (bindingId, options) => {
12850
13543
  const ctx = buildContext();
12851
13544
  const ref = await resolveOrg(ctx, options.org);
12852
13545
  await confirmDestructive(options.yes, `Delete binding ${bindingId}? Values already pushed stay on the destination.`);
12853
13546
  await ctx.client.deleteSyncBinding(ref.id, bindingId);
12854
13547
  console.error(`${bindingId} deleted`);
12855
13548
  });
12856
- sync.command("run <bindingId>").description("push now, synchronously, and report what landed").option("--org <slug>").option("--json", "print the raw API response").action(async (bindingId, options) => {
13549
+ sync.command("run <bindingId>").description("push now, synchronously, and report what landed").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (bindingId, options) => {
12857
13550
  const ctx = buildContext();
12858
13551
  const ref = await resolveOrg(ctx, options.org);
12859
13552
  const { run } = await ctx.client.runSyncBinding(ref.id, bindingId);
@@ -12872,7 +13565,7 @@ function registerSyncCommands(program) {
12872
13565
  });
12873
13566
  if (run.status !== "succeeded") process.exitCode = 1;
12874
13567
  });
12875
- sync.command("runs").description("show the sync run history").option("--org <slug>").option("--binding <id>", "only runs of this binding").option("--json", "print the raw API response").action(async (options) => {
13568
+ sync.command("runs").description("show the sync run history").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--binding <id>", "only runs of this binding").option("--json", "print the raw API response").action(async (options) => {
12876
13569
  const ctx = buildContext();
12877
13570
  const ref = await resolveOrg(ctx, options.org);
12878
13571
  const { runs } = await ctx.client.listSyncRuns(ref.id, options.binding);
@@ -13038,6 +13731,7 @@ async function runLogout() {
13038
13731
  console.error("not signed in");
13039
13732
  return;
13040
13733
  }
13734
+ if (!sessionToken) console.error(config.token ? "signed out — the stored service token was removed from this machine" : "signed out");
13041
13735
  if (sessionToken) {
13042
13736
  const api = new SeekritClient({
13043
13737
  baseUrl: process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "https://api.seekrit.dev",
@@ -13255,9 +13949,10 @@ withTarget(secrets.command("restore <name> <version>").description("roll a secre
13255
13949
  const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, name, parseVersion(version));
13256
13950
  console.error(`${name} restored from v${restoredFrom} — now v${secret.version}`);
13257
13951
  });
13258
- withTarget(secrets.command("rm <name>").description("delete a secret")).action(async (name, options) => {
13952
+ withTarget(secrets.command("rm <name>").description("delete a secret and its whole version history").option("-y, --yes", "skip the confirmation")).action(async (name, options) => {
13259
13953
  const ctx = buildContext();
13260
- const { orgId, envId } = await resolveEnvTarget(ctx, options);
13954
+ const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
13955
+ await confirmDestructive(options.yes, `Delete ${name} from ${label}, including every earlier version? This cannot be undone.`);
13261
13956
  await ctx.client.deleteSecret(orgId, envId, name);
13262
13957
  console.error(`${name} deleted`);
13263
13958
  });
@@ -13342,8 +14037,7 @@ async function materializeForRun(options) {
13342
14037
  branch
13343
14038
  }, dotenvVars);
13344
14039
  } catch (err) {
13345
- const message = err instanceof Error ? err.message : String(err);
13346
- console.error(`seekrit: continuing without seekrit-managed secrets: ${message}`);
14040
+ console.error(`seekrit: continuing without seekrit-managed secrets: ${describeError(err)}`);
13347
14041
  const values = {};
13348
14042
  const provenance = {};
13349
14043
  overlayEnvFiles(values, provenance, envFiles);
@@ -13463,7 +14157,7 @@ async function reapStragglers(pids, signal) {
13463
14157
  process.kill(pid, "SIGKILL");
13464
14158
  } catch {}
13465
14159
  }
13466
- program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
14160
+ program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
13467
14161
  const [cmd, ...args] = commandParts;
13468
14162
  if (!cmd) fail("no command given");
13469
14163
  const { values, provenance, interpolated, unresolvedRefs } = await materializeForRun(options);
@@ -13515,7 +14209,7 @@ program.command("run").description("run a command with decrypted secrets injecte
13515
14209
  });
13516
14210
  child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
13517
14211
  });
13518
- program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--org <slug>").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
14212
+ program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
13519
14213
  if (![
13520
14214
  "dotenv",
13521
14215
  "json",
@@ -13531,7 +14225,7 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
13531
14225
  registerAccessCommands(program);
13532
14226
  registerHoneyTokenCommands(program);
13533
14227
  const token = program.command("token").description("manage service tokens (CI, docker, agents)");
13534
- token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
14228
+ token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
13535
14229
  const ctx = buildContext();
13536
14230
  const role = options.admin ? "admin" : "member";
13537
14231
  const boundToEnv = Boolean(options.app || options.env);
@@ -13580,7 +14274,7 @@ token.command("create").description("create a service token (runtime, or --admin
13580
14274
  console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
13581
14275
  console.log(created.token);
13582
14276
  });
13583
- token.command("list").alias("ls").description("list service tokens").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
14277
+ token.command("list").alias("ls").description("list service tokens").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
13584
14278
  const ctx = buildContext();
13585
14279
  const orgRef = await resolveOrg(ctx, options.org);
13586
14280
  const { tokens } = await ctx.client.listTokens(orgRef.id);
@@ -13594,15 +14288,17 @@ token.command("list").alias("ls").description("list service tokens").option("--o
13594
14288
  col("id", (t) => t.id)
13595
14289
  ], "no service tokens — create one with `seekrit token create`"));
13596
14290
  });
13597
- token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>").action(async (tokenId, options) => {
14291
+ token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (tokenId, options) => {
13598
14292
  const ctx = buildContext();
13599
14293
  const orgRef = await resolveOrg(ctx, options.org);
14294
+ await confirmDestructive(options.yes, `Revoke ${tokenId}? Anything still presenting it stops resolving immediately.`);
13600
14295
  await ctx.client.revokeToken(orgRef.id, tokenId);
13601
14296
  console.error(`${tokenId} revoked`);
13602
14297
  });
13603
- token.command("delete <tokenId>").description("permanently delete a revoked service token (revoke it first)").option("--org <slug>").action(async (tokenId, options) => {
14298
+ token.command("delete <tokenId>").description("permanently delete a revoked service token (revoke it first)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (tokenId, options) => {
13604
14299
  const ctx = buildContext();
13605
14300
  const orgRef = await resolveOrg(ctx, options.org);
14301
+ await confirmDestructive(options.yes, `Permanently delete ${tokenId}? Its audit history keeps the id, but the token row is gone.`);
13606
14302
  await ctx.client.deleteToken(orgRef.id, tokenId);
13607
14303
  console.error(`${tokenId} deleted`);
13608
14304
  });
@@ -13614,6 +14310,8 @@ registerProvisionerCommands(program);
13614
14310
  registerProxyCommands(program);
13615
14311
  registerAgentCommands(program);
13616
14312
  registerPaperclipCommands(program);
14313
+ registerOpenclawCommands(program);
14314
+ registerHermesCommands(program);
13617
14315
  registerSshCommands(program);
13618
14316
  registerAwsCommands(program);
13619
14317
  registerGcpCommands(program);
@@ -13633,7 +14331,7 @@ registerSyncCommands(program);
13633
14331
  registerBillingCommands(program);
13634
14332
  const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
13635
14333
  program.parseAsync(argv).catch((err) => {
13636
- fail(err instanceof Error ? err.message : String(err));
14334
+ fail(describeError(err));
13637
14335
  });
13638
14336
  //#endregion
13639
14337
  export { verifyMessage as A, toBase64 as B, isServiceToken as C, importVerifyingKey as D, importSigningKey as E, kmsBlobKeyRef as F, kmsDecrypt as I, kmsEncrypt as L, generateMysqlCredential as M, generateDataKey as N, signMessage as O, generateEncryptKeyMaterial as P, wrapDek as R, createServiceToken as S, generateSigningKeyMaterial as T, parseBranchTtl as V, isTokenAuth as _, ensureM2mAdminToken as a, writeProjectConfig as b, kmsResolveKey as c, resolveAppEnv as d, resolveBranch as f, getDek as g, resolveOrg as h, materializeEnv as i, generatePostgresCredential as j, signatureKeyRef as k, kmsResolveRecipient as l, resolveGroup as m, fetchDecryptedSecrets as n, kmsCallerIdentity as o, resolveEnvTarget as p, fetchDecryptedVersion as r, kmsRecoverMaterial as s, encryptAndSetSecret as t, resolveApp as u, tryBuildContext as v, parseServiceToken as w, version as x, setFailThrows as y, generateDek as z };