@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 +58 -10
- package/dist/{mcp-CbNXG37n.js → mcp-t-sJ_SvE.js} +49 -4
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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-
|
|
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 {
|
|
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
|
|
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()
|