@seekrit/mcp 0.5.0 → 0.6.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 +75 -8
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -664,7 +664,7 @@ function isServiceToken(value) {
664
664
  }
665
665
  //#endregion
666
666
  //#region ../cli/package.json
667
- var version$1 = "0.23.0";
667
+ var version$1 = "0.24.0";
668
668
  const PROJECT_FILE = "seekrit.json";
669
669
  function globalConfigPath() {
670
670
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -886,6 +886,19 @@ var SeekritClient = class {
886
886
  deleteSecret(orgId, envId, name) {
887
887
  return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
888
888
  }
889
+ /** A secret's append-only history, newest version first. */
890
+ listSecretVersions(orgId, envId, name, query = {}) {
891
+ const qs = query.limit === void 0 ? "" : `?limit=${query.limit}`;
892
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/versions${qs}`);
893
+ }
894
+ /**
895
+ * Roll a secret back to an earlier version. Keyless — the server copies the
896
+ * ciphertext it already stores, so this appends a new version rather than
897
+ * rewinding, and needs no DEK on the caller's side.
898
+ */
899
+ restoreSecret(orgId, envId, name, version) {
900
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/restore`, { version });
901
+ }
889
902
  /** The calling principal's wrapped DEK for this environment. */
890
903
  getMyEnvKey(orgId, envId) {
891
904
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
@@ -1429,14 +1442,23 @@ function parseDotenv(content) {
1429
1442
  }
1430
1443
  return out;
1431
1444
  }
1432
- //#endregion
1433
- //#region ../cli/src/secrets.ts
1434
1445
  /** Fetch + decrypt every secret in a single environment. */
1435
1446
  async function fetchDecryptedSecrets(ctx, orgId, envId) {
1436
1447
  const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
1437
1448
  const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
1438
1449
  return Object.fromEntries(entries);
1439
1450
  }
1451
+ /**
1452
+ * Decrypt one historical version of a secret. Ciphertext is bound to
1453
+ * `(envId, name)` as AAD and neither changes across versions, so an old blob
1454
+ * opens with the environment's current data key — no special handling needed.
1455
+ */
1456
+ async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
1457
+ const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
1458
+ const row = versions.find((v) => v.version === version);
1459
+ if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
1460
+ return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
1461
+ }
1440
1462
  async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
1441
1463
  const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
1442
1464
  await ctx.client.setSecret(orgId, envId, name, ciphertext);
@@ -1563,6 +1585,8 @@ function getStartedText() {
1563
1585
  "## 3. Store secrets",
1564
1586
  "- `set_secret` — encrypts a value locally and stores the ciphertext.",
1565
1587
  "- `list_secrets` — confirm names + versions (never returns values).",
1588
+ "- `list_secret_versions` + `restore_secret` — undo a bad write by rolling",
1589
+ " back to an earlier version (keyless, and history is append-only).",
1566
1590
  "",
1567
1591
  "## 4. Use secrets without exposing them",
1568
1592
  "- `run_command -- <cmd>` — inject secrets into a subprocess; prefer this.",
@@ -2075,10 +2099,11 @@ async function runMcpServer(options = {}) {
2075
2099
  name: o.name
2076
2100
  };
2077
2101
  });
2078
- 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).", {
2102
+ 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.", {
2079
2103
  ...targetShape,
2080
2104
  name: z.string(),
2081
- reveal: z.boolean().optional()
2105
+ reveal: z.boolean().optional(),
2106
+ version: z.number().int().positive().optional().describe("an earlier version from list_secret_versions (default: current)")
2082
2107
  }, async (o) => {
2083
2108
  const ctx = getCtx();
2084
2109
  const { orgId, envId } = await resolveTargetEnv(ctx, o);
@@ -2088,11 +2113,20 @@ async function runMcpServer(options = {}) {
2088
2113
  if (!row) throw new Error(`no secret named ${o.name}`);
2089
2114
  return {
2090
2115
  name: row.name,
2091
- version: row.version,
2116
+ version: o.version ?? row.version,
2092
2117
  revealed: false
2093
2118
  };
2094
2119
  }
2095
2120
  ensureDecryptable(ctx);
2121
+ if (o.version !== void 0) {
2122
+ const value = await fetchDecryptedVersion(ctx, orgId, envId, o.name, o.version);
2123
+ return {
2124
+ name: o.name,
2125
+ version: o.version,
2126
+ value,
2127
+ revealed: true
2128
+ };
2129
+ }
2096
2130
  const values = await fetchDecryptedSecrets(ctx, orgId, envId);
2097
2131
  if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
2098
2132
  return {
@@ -2101,6 +2135,39 @@ async function runMcpServer(options = {}) {
2101
2135
  revealed: true
2102
2136
  };
2103
2137
  });
2138
+ 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.", {
2139
+ ...targetShape,
2140
+ name: z.string(),
2141
+ limit: z.number().int().min(1).max(200).optional().describe("default 20")
2142
+ }, async (o) => {
2143
+ const ctx = getCtx();
2144
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
2145
+ const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, o.name, { limit: o.limit ?? 20 });
2146
+ return {
2147
+ currentVersion,
2148
+ versions: versions.map((v) => ({
2149
+ version: v.version,
2150
+ createdAt: v.createdAt,
2151
+ createdBy: `${v.createdByType}:${v.createdById}`,
2152
+ restoredFromVersion: v.restoredFromVersion
2153
+ }))
2154
+ };
2155
+ });
2156
+ 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.", {
2157
+ ...targetShape,
2158
+ name: z.string(),
2159
+ version: z.number().int().positive()
2160
+ }, async (o) => {
2161
+ const ctx = getCtx();
2162
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
2163
+ const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, o.name, o.version);
2164
+ return {
2165
+ ok: true,
2166
+ name: o.name,
2167
+ restoredFrom,
2168
+ version: secret.version
2169
+ };
2170
+ });
2104
2171
  tool("delete_secret", "Delete a secret from an environment.", {
2105
2172
  ...targetShape,
2106
2173
  name: z.string()
@@ -2389,7 +2456,7 @@ async function runMcpServer(options = {}) {
2389
2456
  /**
2390
2457
  * `@seekrit/mcp` — a standalone, `npx`-able entrypoint for seekrit's MCP server.
2391
2458
  *
2392
- * This is a thin wrapper: the server itself (all 27 tools, the crypto plane of
2459
+ * This is a thin wrapper: the server itself (every tool, the crypto plane of
2393
2460
  * the two-server design) lives in `@seekrit/cli` and is shared with the
2394
2461
  * `seekrit mcp` subcommand — this package just publishes it as its own binary so
2395
2462
  * an agent can run it with zero prior install:
@@ -2402,7 +2469,7 @@ async function runMcpServer(options = {}) {
2402
2469
  * `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
2403
2470
  * published package is self-contained and needs no `@seekrit/cli` install.
2404
2471
  */
2405
- runMcpServer({ version: "0.5.0" }).catch((err) => {
2472
+ runMcpServer({ version: "0.6.0" }).catch((err) => {
2406
2473
  const message = err instanceof Error ? err.message : String(err);
2407
2474
  process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
2408
2475
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "npx-able MCP server for seekrit — let Claude Code and other MCP clients provision, manage, and inject end-to-end encrypted secrets.",
5
5
  "mcpName": "dev.seekrit/mcp",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.1.0",
25
25
  "tsdown": "^0.22.3",
26
- "@seekrit/cli": "0.23.0"
26
+ "@seekrit/cli": "0.24.0"
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsdown",