@seekrit/cli 0.23.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -708,6 +708,8 @@ z.object({
708
708
  z.object({
709
709
  /** Opaque versioned ciphertext blob from @seekrit/crypto. */
710
710
  ciphertext: z.string().min(1).max(65536) });
711
+ z.object({ version: z.number().int().positive() });
712
+ z.object({ limit: z.coerce.number().int().min(1).max(200).default(50) });
711
713
  z.object({
712
714
  publicKeyJwk: z.string().min(1),
713
715
  /**
@@ -1152,7 +1154,7 @@ function encryptAad(ref, context) {
1152
1154
  function dataKeyAad(keyId, version) {
1153
1155
  return `${keyId}/${version}`;
1154
1156
  }
1155
- function parseVersion(versionStr) {
1157
+ function parseVersion$1(versionStr) {
1156
1158
  const version = Number(versionStr);
1157
1159
  if (!Number.isInteger(version) || version < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid key version in KMS blob");
1158
1160
  return version;
@@ -1164,7 +1166,7 @@ function kmsBlobKeyRef(blob) {
1164
1166
  const [keyId, versionStr] = splitBlob(blob, prefix, 4);
1165
1167
  return {
1166
1168
  keyId,
1167
- version: parseVersion(versionStr)
1169
+ version: parseVersion$1(versionStr)
1168
1170
  };
1169
1171
  }
1170
1172
  /** Encrypt a value under a managed key. `context` (bound as AAD) defaults to empty. */
@@ -1193,7 +1195,7 @@ async function kmsDecrypt(material, blob, context = "") {
1193
1195
  const [keyId, versionStr, ivB64, ctB64] = splitBlob(blob, ENCRYPT_PREFIX, 4);
1194
1196
  const ref = {
1195
1197
  keyId,
1196
- version: parseVersion(versionStr)
1198
+ version: parseVersion$1(versionStr)
1197
1199
  };
1198
1200
  const key = await importAesKey(material, "decrypt");
1199
1201
  try {
@@ -1976,7 +1978,7 @@ function isServiceToken(value) {
1976
1978
  }
1977
1979
  //#endregion
1978
1980
  //#region package.json
1979
- var version = "0.23.4";
1981
+ var version = "0.24.0";
1980
1982
  //#endregion
1981
1983
  //#region ../../packages/api-client/src/index.ts
1982
1984
  var SeekritApiError = class extends Error {
@@ -2153,6 +2155,19 @@ var SeekritClient = class {
2153
2155
  deleteSecret(orgId, envId, name) {
2154
2156
  return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
2155
2157
  }
2158
+ /** A secret's append-only history, newest version first. */
2159
+ listSecretVersions(orgId, envId, name, query = {}) {
2160
+ const qs = query.limit === void 0 ? "" : `?limit=${query.limit}`;
2161
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/versions${qs}`);
2162
+ }
2163
+ /**
2164
+ * Roll a secret back to an earlier version. Keyless — the server copies the
2165
+ * ciphertext it already stores, so this appends a new version rather than
2166
+ * rewinding, and needs no DEK on the caller's side.
2167
+ */
2168
+ restoreSecret(orgId, envId, name, version) {
2169
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/restore`, { version });
2170
+ }
2156
2171
  /** The calling principal's wrapped DEK for this environment. */
2157
2172
  getMyEnvKey(orgId, envId) {
2158
2173
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
@@ -4080,14 +4095,23 @@ function collect$1(value, acc) {
4080
4095
  acc.push(value);
4081
4096
  return acc;
4082
4097
  }
4083
- //#endregion
4084
- //#region src/secrets.ts
4085
4098
  /** Fetch + decrypt every secret in a single environment. */
4086
4099
  async function fetchDecryptedSecrets(ctx, orgId, envId) {
4087
4100
  const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
4088
4101
  const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
4089
4102
  return Object.fromEntries(entries);
4090
4103
  }
4104
+ /**
4105
+ * Decrypt one historical version of a secret. Ciphertext is bound to
4106
+ * `(envId, name)` as AAD and neither changes across versions, so an old blob
4107
+ * opens with the environment's current data key — no special handling needed.
4108
+ */
4109
+ async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
4110
+ const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
4111
+ const row = versions.find((v) => v.version === version);
4112
+ if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
4113
+ return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
4114
+ }
4091
4115
  async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
4092
4116
  const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
4093
4117
  await ctx.client.setSecret(orgId, envId, name, ciphertext);
@@ -4480,6 +4504,12 @@ group.command("env").description("manage a group’s environments (per-slug valu
4480
4504
  });
4481
4505
  console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
4482
4506
  });
4507
+ /** Parse a positive integer flag/argument (version numbers, page sizes). */
4508
+ function parseVersion(raw) {
4509
+ const n = Number(raw);
4510
+ if (!Number.isInteger(n) || n < 1) fail(`expected a positive whole number, got "${raw}"`);
4511
+ return n;
4512
+ }
4483
4513
  /** Attach the environment-selection flags shared by every `secrets` command. */
4484
4514
  function withTarget(cmd) {
4485
4515
  return cmd.option("--org <slug>", "organization slug").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");
@@ -4491,10 +4521,12 @@ withTarget(secrets.command("list").description("list secret names (no values)"))
4491
4521
  const { secrets: rows } = await ctx.client.listSecrets(orgId, envId);
4492
4522
  for (const row of rows) console.log(`${row.name}\tv${row.version}\t${row.updatedAt}`);
4493
4523
  });
4494
- withTarget(secrets.command("get <name>").description("decrypt and print one secret value")).action(async (name, options) => {
4524
+ withTarget(secrets.command("get <name>").description("decrypt and print one secret value").option("--version <n>", "print an earlier version instead of the current one")).action(async (name, options) => {
4495
4525
  const ctx = buildContext();
4496
4526
  const { orgId, envId } = await resolveEnvTarget(ctx, options);
4497
- const value = (await fetchDecryptedSecrets(ctx, orgId, envId))[name];
4527
+ let value;
4528
+ if (options.version === void 0) value = (await fetchDecryptedSecrets(ctx, orgId, envId))[name];
4529
+ else value = await fetchDecryptedVersion(ctx, orgId, envId, name, parseVersion(options.version));
4498
4530
  if (value === void 0) fail(`no secret named ${name}`);
4499
4531
  process.stdout.write(value);
4500
4532
  if (process.stdout.isTTY) process.stdout.write("\n");
@@ -4530,6 +4562,22 @@ withTarget(secrets.command("import [file]").description("bulk-import secrets fro
4530
4562
  const { created, updated } = await importSecrets(ctx, orgId, envId, entries);
4531
4563
  console.error(`imported ${created.length + updated.length} secret(s) into ${label} (${created.length} new, ${updated.length} updated)`);
4532
4564
  });
4565
+ withTarget(secrets.command("history <name>").description("list a secret's versions (metadata only — no values)").option("--limit <n>", `how many versions to show (max 200)`, "20")).action(async (name, options) => {
4566
+ const ctx = buildContext();
4567
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
4568
+ const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, name, { limit: parseVersion(options.limit) });
4569
+ for (const v of versions) {
4570
+ const marks = [v.version === currentVersion ? "current" : null, v.restoredFromVersion === null ? null : `restored from v${v.restoredFromVersion}`].filter(Boolean);
4571
+ const note = marks.length > 0 ? `\t${marks.join(", ")}` : "";
4572
+ console.log(`v${v.version}\t${v.createdAt}\t${v.createdByType}:${v.createdById}${note}`);
4573
+ }
4574
+ });
4575
+ withTarget(secrets.command("restore <name> <version>").description("roll a secret back to an earlier version (stored as a new version)")).action(async (name, version, options) => {
4576
+ const ctx = buildContext();
4577
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
4578
+ const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, name, parseVersion(version));
4579
+ console.error(`${name} restored from v${restoredFrom} — now v${secret.version}`);
4580
+ });
4533
4581
  withTarget(secrets.command("rm <name>").description("delete a secret")).action(async (name, options) => {
4534
4582
  const ctx = buildContext();
4535
4583
  const { orgId, envId } = await resolveEnvTarget(ctx, options);
@@ -4861,7 +4909,7 @@ registerMongoCommands(program);
4861
4909
  registerKmsCommands(program);
4862
4910
  registerRecoveryCommands(program);
4863
4911
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
4864
- const { runMcpServer } = await import("./mcp-CbNXG37n.js");
4912
+ const { runMcpServer } = await import("./mcp-t-sJ_SvE.js");
4865
4913
  await runMcpServer();
4866
4914
  });
4867
4915
  program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
@@ -4875,4 +4923,4 @@ program.parseAsync(argv).catch((err) => {
4875
4923
  fail(err instanceof Error ? err.message : String(err));
4876
4924
  });
4877
4925
  //#endregion
4878
- export { generateDataKey as A, importSigningKey as C, verifyMessage as D, signatureKeyRef as E, wrapDek as F, generateDek as I, toBase64 as L, kmsBlobKeyRef as M, kmsDecrypt as N, generatePostgresCredential as O, kmsEncrypt as P, generateSigningKeyMaterial as S, signMessage as T, writeProjectConfig as _, kmsCallerIdentity as a, isServiceToken as b, kmsResolveRecipient as c, resolveGroup as d, resolveOrg as f, setFailThrows as g, tryBuildContext as h, ensureM2mAdminToken as i, generateEncryptKeyMaterial as j, generateMysqlCredential as k, resolveAppEnv as l, isTokenAuth as m, fetchDecryptedSecrets as n, kmsRecoverMaterial as o, getDek as p, materializeEnv as r, kmsResolveKey as s, encryptAndSetSecret as t, resolveEnvTarget as u, version as v, importVerifyingKey as w, parseServiceToken as x, createServiceToken as y };
4926
+ export { generateMysqlCredential as A, generateSigningKeyMaterial as C, signatureKeyRef as D, signMessage as E, kmsEncrypt as F, wrapDek as I, generateDek as L, generateEncryptKeyMaterial as M, kmsBlobKeyRef as N, verifyMessage as O, kmsDecrypt as P, toBase64 as R, parseServiceToken as S, importVerifyingKey as T, setFailThrows as _, ensureM2mAdminToken as a, createServiceToken as b, kmsResolveKey as c, resolveEnvTarget as d, resolveGroup as f, tryBuildContext as g, isTokenAuth as h, materializeEnv as i, generateDataKey as j, generatePostgresCredential as k, kmsResolveRecipient as l, getDek as m, fetchDecryptedSecrets as n, kmsCallerIdentity as o, resolveOrg as p, fetchDecryptedVersion as r, kmsRecoverMaterial as s, encryptAndSetSecret as t, resolveAppEnv as u, writeProjectConfig as v, importSigningKey as w, isServiceToken as x, version as y };
@@ -1,4 +1,4 @@
1
- import { A as generateDataKey, C as importSigningKey, D as verifyMessage, E as signatureKeyRef, F as wrapDek, I as generateDek, L as toBase64, M as kmsBlobKeyRef, N as kmsDecrypt, O as generatePostgresCredential, P as kmsEncrypt, S as generateSigningKeyMaterial, T as signMessage, _ as writeProjectConfig, a as kmsCallerIdentity, b as isServiceToken, c as kmsResolveRecipient, d as resolveGroup, f as resolveOrg, g as setFailThrows, h as tryBuildContext, i as ensureM2mAdminToken, j as generateEncryptKeyMaterial, k as generateMysqlCredential, l as resolveAppEnv, m as isTokenAuth, n as fetchDecryptedSecrets, o as kmsRecoverMaterial, p as getDek, r as materializeEnv, s as kmsResolveKey, t as encryptAndSetSecret, u as resolveEnvTarget, v as version, w as importVerifyingKey, x as parseServiceToken, y as createServiceToken } from "./index.js";
1
+ import { A as generateMysqlCredential, C as generateSigningKeyMaterial, D as signatureKeyRef, E as signMessage, F as kmsEncrypt, I as wrapDek, L as generateDek, M as generateEncryptKeyMaterial, N as kmsBlobKeyRef, O as verifyMessage, P as kmsDecrypt, R as toBase64, S as parseServiceToken, T as importVerifyingKey, _ as setFailThrows, a as ensureM2mAdminToken, b as createServiceToken, c as kmsResolveKey, d as resolveEnvTarget, f as resolveGroup, g as tryBuildContext, h as isTokenAuth, i as materializeEnv, j as generateDataKey, k as generatePostgresCredential, l as kmsResolveRecipient, m as getDek, n as fetchDecryptedSecrets, o as kmsCallerIdentity, p as resolveOrg, r as fetchDecryptedVersion, s as kmsRecoverMaterial, t as encryptAndSetSecret, u as resolveAppEnv, v as writeProjectConfig, w as importSigningKey, x as isServiceToken, y as version } from "./index.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -73,6 +73,8 @@ function getStartedText() {
73
73
  "## 3. Store secrets",
74
74
  "- `set_secret` — encrypts a value locally and stores the ciphertext.",
75
75
  "- `list_secrets` — confirm names + versions (never returns values).",
76
+ "- `list_secret_versions` + `restore_secret` — undo a bad write by rolling",
77
+ " back to an earlier version (keyless, and history is append-only).",
76
78
  "",
77
79
  "## 4. Use secrets without exposing them",
78
80
  "- `run_command -- <cmd>` — inject secrets into a subprocess; prefer this.",
@@ -585,10 +587,11 @@ async function runMcpServer(options = {}) {
585
587
  name: o.name
586
588
  };
587
589
  });
588
- tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command).", {
590
+ tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command). Pass `version` to read an earlier version instead of the current one.", {
589
591
  ...targetShape,
590
592
  name: z.string(),
591
- reveal: z.boolean().optional()
593
+ reveal: z.boolean().optional(),
594
+ version: z.number().int().positive().optional().describe("an earlier version from list_secret_versions (default: current)")
592
595
  }, async (o) => {
593
596
  const ctx = getCtx();
594
597
  const { orgId, envId } = await resolveTargetEnv(ctx, o);
@@ -598,11 +601,20 @@ async function runMcpServer(options = {}) {
598
601
  if (!row) throw new Error(`no secret named ${o.name}`);
599
602
  return {
600
603
  name: row.name,
601
- version: row.version,
604
+ version: o.version ?? row.version,
602
605
  revealed: false
603
606
  };
604
607
  }
605
608
  ensureDecryptable(ctx);
609
+ if (o.version !== void 0) {
610
+ const value = await fetchDecryptedVersion(ctx, orgId, envId, o.name, o.version);
611
+ return {
612
+ name: o.name,
613
+ version: o.version,
614
+ value,
615
+ revealed: true
616
+ };
617
+ }
606
618
  const values = await fetchDecryptedSecrets(ctx, orgId, envId);
607
619
  if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
608
620
  return {
@@ -611,6 +623,39 @@ async function runMcpServer(options = {}) {
611
623
  revealed: true
612
624
  };
613
625
  });
626
+ tool("list_secret_versions", "List a secret's version history: who wrote each version, when, and which ones were restores. Never returns values — pair it with restore_secret to roll back, or get_secret(version, reveal:true) to inspect one.", {
627
+ ...targetShape,
628
+ name: z.string(),
629
+ limit: z.number().int().min(1).max(200).optional().describe("default 20")
630
+ }, async (o) => {
631
+ const ctx = getCtx();
632
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
633
+ const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, o.name, { limit: o.limit ?? 20 });
634
+ return {
635
+ currentVersion,
636
+ versions: versions.map((v) => ({
637
+ version: v.version,
638
+ createdAt: v.createdAt,
639
+ createdBy: `${v.createdByType}:${v.createdById}`,
640
+ restoredFromVersion: v.restoredFromVersion
641
+ }))
642
+ };
643
+ });
644
+ tool("restore_secret", "Roll a secret back to an earlier version. The stored ciphertext is replayed as a NEW version (history is append-only, nothing is overwritten). Keyless — no decryption happens, so this works even without a key.", {
645
+ ...targetShape,
646
+ name: z.string(),
647
+ version: z.number().int().positive()
648
+ }, async (o) => {
649
+ const ctx = getCtx();
650
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
651
+ const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, o.name, o.version);
652
+ return {
653
+ ok: true,
654
+ name: o.name,
655
+ restoredFrom,
656
+ version: secret.version
657
+ };
658
+ });
614
659
  tool("delete_secret", "Delete a secret from an environment.", {
615
660
  ...targetShape,
616
661
  name: z.string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.23.4",
3
+ "version": "0.24.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {