@seekrit/cli 1.6.5 → 1.7.1
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 +336 -39
- package/dist/{mcp-BvXwiGRv.js → mcp-ZYu1Q6Ft.js} +35 -14
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -852,6 +852,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
852
852
|
description: "Single sign-on beyond the built-in providers.",
|
|
853
853
|
default: true
|
|
854
854
|
},
|
|
855
|
+
"feature.approvals": {
|
|
856
|
+
kind: "feature",
|
|
857
|
+
label: "Approval workflows",
|
|
858
|
+
description: "Require review before a change to a protected environment lands.",
|
|
859
|
+
default: true
|
|
860
|
+
},
|
|
855
861
|
"feature.sync": {
|
|
856
862
|
kind: "feature",
|
|
857
863
|
label: "Third-party sync",
|
|
@@ -1027,6 +1033,23 @@ function parseBranchTtl(input) {
|
|
|
1027
1033
|
return value * multiplier;
|
|
1028
1034
|
}
|
|
1029
1035
|
//#endregion
|
|
1036
|
+
//#region ../../packages/core/src/change-requests.ts
|
|
1037
|
+
const CHANGE_REQUEST_STATUSES = [
|
|
1038
|
+
"open",
|
|
1039
|
+
"applied",
|
|
1040
|
+
"rejected",
|
|
1041
|
+
"withdrawn",
|
|
1042
|
+
"superseded"
|
|
1043
|
+
];
|
|
1044
|
+
/** Human summary of a queued change, for emails and CLI output. Never a value. */
|
|
1045
|
+
function describeChange(kind, name, restoreFrom) {
|
|
1046
|
+
switch (kind) {
|
|
1047
|
+
case "set": return `set ${name}`;
|
|
1048
|
+
case "delete": return `delete ${name}`;
|
|
1049
|
+
case "restore": return `restore ${name} to v${restoreFrom ?? "?"}`;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1030
1053
|
//#region ../../packages/core/src/dotenv.ts
|
|
1031
1054
|
/**
|
|
1032
1055
|
* `.env` parsing and serialization — the canonical definition of what a `.env`
|
|
@@ -1999,7 +2022,16 @@ const AUDIT_ACTIONS = [
|
|
|
1999
2022
|
"agent.policy_published",
|
|
2000
2023
|
"agent.policy_rolled_back",
|
|
2001
2024
|
"agent.task_dispatched",
|
|
2002
|
-
"agent.task_revoked"
|
|
2025
|
+
"agent.task_revoked",
|
|
2026
|
+
"env.protection_enabled",
|
|
2027
|
+
"env.protection_updated",
|
|
2028
|
+
"env.protection_disabled",
|
|
2029
|
+
"change_request.opened",
|
|
2030
|
+
"change_request.approved",
|
|
2031
|
+
"change_request.rejected",
|
|
2032
|
+
"change_request.withdrawn",
|
|
2033
|
+
"change_request.applied",
|
|
2034
|
+
"change_request.superseded"
|
|
2003
2035
|
];
|
|
2004
2036
|
/**
|
|
2005
2037
|
* Transactional notification emails seekrit can send. Each id is one
|
|
@@ -2019,7 +2051,9 @@ const NOTIFICATION_TYPES = [
|
|
|
2019
2051
|
"lease_expired",
|
|
2020
2052
|
"sync_failed",
|
|
2021
2053
|
"honey_token_tripped",
|
|
2022
|
-
"secret_rotation_failed"
|
|
2054
|
+
"secret_rotation_failed",
|
|
2055
|
+
"change_request_opened",
|
|
2056
|
+
"change_request_decided"
|
|
2023
2057
|
];
|
|
2024
2058
|
/** UI-facing copy + default for each notification type. */
|
|
2025
2059
|
const NOTIFICATION_TYPE_META = {
|
|
@@ -2063,6 +2097,16 @@ const NOTIFICATION_TYPE_META = {
|
|
|
2063
2097
|
description: "A temporary (leased) credential you own expired and was automatically revoked.",
|
|
2064
2098
|
defaultEnabled: true
|
|
2065
2099
|
},
|
|
2100
|
+
change_request_opened: {
|
|
2101
|
+
label: "Change request opened",
|
|
2102
|
+
description: "Someone proposed a change to a protected environment and it is waiting for review.",
|
|
2103
|
+
defaultEnabled: true
|
|
2104
|
+
},
|
|
2105
|
+
change_request_decided: {
|
|
2106
|
+
label: "Change request decided",
|
|
2107
|
+
description: "A change you proposed was approved and applied, rejected, or superseded.",
|
|
2108
|
+
defaultEnabled: true
|
|
2109
|
+
},
|
|
2066
2110
|
sync_failed: {
|
|
2067
2111
|
label: "Third-party sync failed",
|
|
2068
2112
|
description: "A sync to an external destination failed repeatedly and stopped retrying. The destination is now holding stale values.",
|
|
@@ -2130,7 +2174,12 @@ z.object({
|
|
|
2130
2174
|
z.object({
|
|
2131
2175
|
groupId: z.string().min(1),
|
|
2132
2176
|
/** Precedence among an env's groups (higher wins). Appended if omitted. */
|
|
2133
|
-
position: z.number().int().min(0).optional()
|
|
2177
|
+
position: z.number().int().min(0).optional(),
|
|
2178
|
+
/** Client-wrapped group DEK for each sync connection newly reading this group. */
|
|
2179
|
+
syncGrants: z.array(z.object({
|
|
2180
|
+
connectionId: z.string().min(1),
|
|
2181
|
+
wrappedDek: z.string().min(1)
|
|
2182
|
+
})).optional()
|
|
2134
2183
|
});
|
|
2135
2184
|
z.object({
|
|
2136
2185
|
name: nameSchema,
|
|
@@ -2149,6 +2198,17 @@ z.object({
|
|
|
2149
2198
|
ciphertext: z.string().min(1).max(65536) });
|
|
2150
2199
|
z.object({ version: z.number().int().positive() });
|
|
2151
2200
|
z.object({ limit: z.coerce.number().int().min(1).max(200).default(50) });
|
|
2201
|
+
z.object({
|
|
2202
|
+
requiredApprovals: z.number().int().min(1).max(10).default(1),
|
|
2203
|
+
allowSelfApproval: z.boolean().default(false)
|
|
2204
|
+
});
|
|
2205
|
+
z.object({ comment: z.string().trim().max(1e3).optional() });
|
|
2206
|
+
z.string().trim().min(1).max(1e3);
|
|
2207
|
+
z.object({
|
|
2208
|
+
status: z.enum(CHANGE_REQUEST_STATUSES).optional(),
|
|
2209
|
+
envId: z.string().min(1).optional(),
|
|
2210
|
+
limit: z.coerce.number().int().min(1).max(200).default(50)
|
|
2211
|
+
});
|
|
2152
2212
|
z.object({
|
|
2153
2213
|
publicKeyJwk: z.string().min(1),
|
|
2154
2214
|
/**
|
|
@@ -5055,9 +5115,18 @@ async function createAgentTaskToken() {
|
|
|
5055
5115
|
}
|
|
5056
5116
|
//#endregion
|
|
5057
5117
|
//#region package.json
|
|
5058
|
-
var version = "1.
|
|
5118
|
+
var version = "1.7.1";
|
|
5059
5119
|
//#endregion
|
|
5060
5120
|
//#region ../../packages/api-client/src/index.ts
|
|
5121
|
+
/**
|
|
5122
|
+
* The proposer's note travels as a header, not a body field: an unprotected
|
|
5123
|
+
* environment ignores it, so the same call works either way and no caller has
|
|
5124
|
+
* to know in advance whether a write will be queued.
|
|
5125
|
+
*/
|
|
5126
|
+
function changeNoteHeader(note) {
|
|
5127
|
+
const trimmed = note?.trim();
|
|
5128
|
+
return trimmed ? { "x-seekrit-change-note": trimmed } : void 0;
|
|
5129
|
+
}
|
|
5061
5130
|
var SeekritApiError = class extends Error {
|
|
5062
5131
|
status;
|
|
5063
5132
|
code;
|
|
@@ -5079,8 +5148,11 @@ var SeekritClient = class {
|
|
|
5079
5148
|
this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
|
|
5080
5149
|
this.client = options.client;
|
|
5081
5150
|
}
|
|
5082
|
-
async request(method, path, body) {
|
|
5083
|
-
const headers = {
|
|
5151
|
+
async request(method, path, body, extraHeaders) {
|
|
5152
|
+
const headers = {
|
|
5153
|
+
accept: "application/json",
|
|
5154
|
+
...extraHeaders
|
|
5155
|
+
};
|
|
5084
5156
|
if (this.client) headers["x-seekrit-client"] = this.client;
|
|
5085
5157
|
if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
|
|
5086
5158
|
else if (this.auth.type === "dynamic") {
|
|
@@ -5295,11 +5367,21 @@ var SeekritClient = class {
|
|
|
5295
5367
|
listSecrets(orgId, envId) {
|
|
5296
5368
|
return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets`);
|
|
5297
5369
|
}
|
|
5298
|
-
|
|
5299
|
-
|
|
5370
|
+
/**
|
|
5371
|
+
* Store a ciphertext. In a **protected** environment this does not write: the
|
|
5372
|
+
* server queues the change for review and answers `{ queued: true,
|
|
5373
|
+
* changeRequest }` (HTTP 202). Callers must branch on `queued` — reporting
|
|
5374
|
+
* success on a queued change tells someone a secret rotated when it has not.
|
|
5375
|
+
*
|
|
5376
|
+
* `note` is the proposer's explanation, carried as a header so it is simply
|
|
5377
|
+
* ignored by an unprotected environment.
|
|
5378
|
+
*/
|
|
5379
|
+
setSecret(orgId, envId, name, ciphertext, note) {
|
|
5380
|
+
return this.request("PUT", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`, { ciphertext }, changeNoteHeader(note));
|
|
5300
5381
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5382
|
+
/** Queued for review in a protected environment — see {@link setSecret}. */
|
|
5383
|
+
deleteSecret(orgId, envId, name, note) {
|
|
5384
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`, void 0, changeNoteHeader(note));
|
|
5303
5385
|
}
|
|
5304
5386
|
/** A secret's append-only history, newest version first. */
|
|
5305
5387
|
listSecretVersions(orgId, envId, name, query = {}) {
|
|
@@ -5311,8 +5393,45 @@ var SeekritClient = class {
|
|
|
5311
5393
|
* ciphertext it already stores, so this appends a new version rather than
|
|
5312
5394
|
* rewinding, and needs no DEK on the caller's side.
|
|
5313
5395
|
*/
|
|
5314
|
-
restoreSecret(orgId, envId, name, version) {
|
|
5315
|
-
return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/restore`, { version });
|
|
5396
|
+
restoreSecret(orgId, envId, name, version, note) {
|
|
5397
|
+
return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}/restore`, { version }, changeNoteHeader(note));
|
|
5398
|
+
}
|
|
5399
|
+
/** An environment's protection settings, plus how many changes are waiting. */
|
|
5400
|
+
getEnvironmentProtection(orgId, envId) {
|
|
5401
|
+
return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/protection`);
|
|
5402
|
+
}
|
|
5403
|
+
/** Protect an environment, or change its settings. Admin only. */
|
|
5404
|
+
setEnvironmentProtection(orgId, envId, input = {}) {
|
|
5405
|
+
return this.request("PUT", `/v1/orgs/${orgId}/envs/${envId}/protection`, input);
|
|
5406
|
+
}
|
|
5407
|
+
/** Take the gate down. Queued changes stay queued. Admin only. */
|
|
5408
|
+
deleteEnvironmentProtection(orgId, envId) {
|
|
5409
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/protection`);
|
|
5410
|
+
}
|
|
5411
|
+
listChangeRequests(orgId, query = {}) {
|
|
5412
|
+
const params = new URLSearchParams();
|
|
5413
|
+
if (query.status) params.set("status", query.status);
|
|
5414
|
+
if (query.envId) params.set("envId", query.envId);
|
|
5415
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
5416
|
+
const qs = params.size > 0 ? `?${params}` : "";
|
|
5417
|
+
return this.request("GET", `/v1/orgs/${orgId}/change-requests${qs}`);
|
|
5418
|
+
}
|
|
5419
|
+
getChangeRequest(orgId, id) {
|
|
5420
|
+
return this.request("GET", `/v1/orgs/${orgId}/change-requests/${id}`);
|
|
5421
|
+
}
|
|
5422
|
+
/**
|
|
5423
|
+
* Approve. When this is the approval that meets the threshold the change is
|
|
5424
|
+
* applied in the same call, so `status` comes back `applied` — or
|
|
5425
|
+
* `superseded`, if the secret moved on since the change was proposed.
|
|
5426
|
+
*/
|
|
5427
|
+
approveChangeRequest(orgId, id, comment) {
|
|
5428
|
+
return this.request("POST", `/v1/orgs/${orgId}/change-requests/${id}/approve`, { comment });
|
|
5429
|
+
}
|
|
5430
|
+
rejectChangeRequest(orgId, id, comment) {
|
|
5431
|
+
return this.request("POST", `/v1/orgs/${orgId}/change-requests/${id}/reject`, { comment });
|
|
5432
|
+
}
|
|
5433
|
+
withdrawChangeRequest(orgId, id) {
|
|
5434
|
+
return this.request("POST", `/v1/orgs/${orgId}/change-requests/${id}/withdraw`);
|
|
5316
5435
|
}
|
|
5317
5436
|
/** The calling principal's wrapped DEK for this environment. */
|
|
5318
5437
|
getMyEnvKey(orgId, envId) {
|
|
@@ -7265,9 +7384,15 @@ async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
|
|
|
7265
7384
|
if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
|
|
7266
7385
|
return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
|
|
7267
7386
|
}
|
|
7268
|
-
|
|
7387
|
+
/**
|
|
7388
|
+
* Encrypt and store one value. Returns the change request when the environment
|
|
7389
|
+
* is protected and the write was queued instead of stored — callers must say so
|
|
7390
|
+
* rather than reporting a write that has not happened.
|
|
7391
|
+
*/
|
|
7392
|
+
async function encryptAndSetSecret(ctx, orgId, envId, name, value, note) {
|
|
7269
7393
|
const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
|
|
7270
|
-
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
7394
|
+
const result = await ctx.client.setSecret(orgId, envId, name, ciphertext, note);
|
|
7395
|
+
return result.queued ? result.changeRequest : null;
|
|
7271
7396
|
}
|
|
7272
7397
|
/**
|
|
7273
7398
|
* Encrypt and store many secrets into one environment. The DEK is fetched once
|
|
@@ -7276,16 +7401,18 @@ async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
|
|
|
7276
7401
|
* overwritten; the result splits them into created vs. updated for a summary.
|
|
7277
7402
|
* Callers validate the names first — a rejected name aborts before any write.
|
|
7278
7403
|
*/
|
|
7279
|
-
async function importSecrets(ctx, orgId, envId, entries) {
|
|
7404
|
+
async function importSecrets(ctx, orgId, envId, entries, note) {
|
|
7280
7405
|
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
7281
7406
|
const existing = new Set(secrets.map((s) => s.name));
|
|
7282
7407
|
const result = {
|
|
7283
7408
|
created: [],
|
|
7284
|
-
updated: []
|
|
7409
|
+
updated: [],
|
|
7410
|
+
queued: []
|
|
7285
7411
|
};
|
|
7286
7412
|
for (const [name, value] of Object.entries(entries)) {
|
|
7287
7413
|
const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
|
|
7288
|
-
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
7414
|
+
const written = await ctx.client.setSecret(orgId, envId, name, ciphertext, note);
|
|
7415
|
+
if (written.queued) result.queued.push(written.changeRequest);
|
|
7289
7416
|
(existing.has(name) ? result.updated : result.created).push(name);
|
|
7290
7417
|
}
|
|
7291
7418
|
return result;
|
|
@@ -7524,6 +7651,32 @@ function registerEnvDiff(env) {
|
|
|
7524
7651
|
});
|
|
7525
7652
|
}
|
|
7526
7653
|
//#endregion
|
|
7654
|
+
//#region src/group-sync-grants.ts
|
|
7655
|
+
/** Wrap a newly composed group layer for connections already syncing the app env or its branches. */
|
|
7656
|
+
async function syncGrantsForGroupLink(ctx, orgId, envId, envSlug, groupId) {
|
|
7657
|
+
const [{ groups }, { bindings }, { branches }] = await Promise.all([
|
|
7658
|
+
ctx.client.listEnvGroups(orgId, envId),
|
|
7659
|
+
ctx.client.listSyncBindings(orgId),
|
|
7660
|
+
ctx.client.listBranches(orgId, envId)
|
|
7661
|
+
]);
|
|
7662
|
+
if (groups.some((group) => group.groupId === groupId)) return void 0;
|
|
7663
|
+
const targets = /* @__PURE__ */ new Set([envId, ...branches.map((branch) => branch.id)]);
|
|
7664
|
+
const connectionIds = new Set(bindings.filter((binding) => targets.has(binding.environmentId)).map((b) => b.connectionId));
|
|
7665
|
+
if (connectionIds.size === 0) return void 0;
|
|
7666
|
+
const [{ environments }, { connections }] = await Promise.all([ctx.client.listGroupEnvs(orgId, groupId), ctx.client.listSyncConnections(orgId)]);
|
|
7667
|
+
const groupEnv = environments.find((entry) => entry.slug === envSlug);
|
|
7668
|
+
if (!groupEnv) fail(`create a ${envSlug} environment in this group before composing it`);
|
|
7669
|
+
const dek = await getDek(ctx, orgId, groupEnv.id);
|
|
7670
|
+
return Promise.all([...connectionIds].map(async (connectionId) => {
|
|
7671
|
+
const connection = connections.find((entry) => entry.id === connectionId);
|
|
7672
|
+
if (!connection) fail("a sync connection could not be found; refresh and retry");
|
|
7673
|
+
return {
|
|
7674
|
+
connectionId,
|
|
7675
|
+
wrappedDek: await wrapDek(dek, connection.publicKeyJwk)
|
|
7676
|
+
};
|
|
7677
|
+
}));
|
|
7678
|
+
}
|
|
7679
|
+
//#endregion
|
|
7527
7680
|
//#region src/kms.ts
|
|
7528
7681
|
/** Collect a repeatable option into a list. */
|
|
7529
7682
|
function collect$8(value, acc = []) {
|
|
@@ -8186,9 +8339,11 @@ function registerAppCommands(program) {
|
|
|
8186
8339
|
org: options.org,
|
|
8187
8340
|
group: options.group
|
|
8188
8341
|
});
|
|
8342
|
+
const syncGrants = await syncGrantsForGroupLink(ctx, target.orgId, target.envId, target.envSlug, group.id);
|
|
8189
8343
|
await ctx.client.linkEnvGroup(target.orgId, target.envId, {
|
|
8190
8344
|
groupId: group.id,
|
|
8191
|
-
position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10)
|
|
8345
|
+
position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10),
|
|
8346
|
+
syncGrants
|
|
8192
8347
|
});
|
|
8193
8348
|
console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
|
|
8194
8349
|
});
|
|
@@ -9763,6 +9918,133 @@ function registerBranchCommands(program) {
|
|
|
9763
9918
|
console.error(`deleted branch ${app.slug}#${target.slug}`);
|
|
9764
9919
|
});
|
|
9765
9920
|
}
|
|
9921
|
+
//#endregion
|
|
9922
|
+
//#region src/changes.ts
|
|
9923
|
+
/**
|
|
9924
|
+
* Change requests: the review queue in front of protected environments, and the
|
|
9925
|
+
* protection settings that create it.
|
|
9926
|
+
*
|
|
9927
|
+
* Reviewing here is keyless by construction — a change request carries
|
|
9928
|
+
* ciphertext, so the CLI can show a reviewer *what* is changing and who
|
|
9929
|
+
* proposed it without ever holding a DEK. `seekrit changes show` therefore
|
|
9930
|
+
* prints no value, and deliberately does not offer to.
|
|
9931
|
+
*/
|
|
9932
|
+
function registerChangeCommands(program) {
|
|
9933
|
+
const changes = program.command("changes").description("review changes queued for protected environments");
|
|
9934
|
+
changes.command("list", { isDefault: true }).alias("ls").description("list change requests awaiting review").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--status <status>", `filter by status (${CHANGE_REQUEST_STATUSES.join(", ")})`, "open").option("--all", "every status, not just open").option("--limit <n>", "entries to show (max 200)", "50").option("--json", "print the raw API response").action(async (options) => {
|
|
9935
|
+
const ctx = buildContext();
|
|
9936
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
9937
|
+
const status = options.all ? void 0 : parseStatus(options.status);
|
|
9938
|
+
const limit = Number(options.limit);
|
|
9939
|
+
if (!Number.isInteger(limit) || limit < 1) fail("--limit must be a positive whole number");
|
|
9940
|
+
const { changeRequests } = await ctx.client.listChangeRequests(ref.id, {
|
|
9941
|
+
status,
|
|
9942
|
+
limit: Math.min(limit, 200)
|
|
9943
|
+
});
|
|
9944
|
+
emit(options, { changeRequests }, () => {
|
|
9945
|
+
printTable(changeRequests, [
|
|
9946
|
+
col("id", (r) => r.id),
|
|
9947
|
+
col("change", (r) => describeChange(r.kind, r.secretName, r.restoreFromVersion)),
|
|
9948
|
+
col("status", (r) => r.status),
|
|
9949
|
+
col("proposed by", (r) => `${r.requestedByType}:${r.requestedById}`),
|
|
9950
|
+
col("when", (r) => r.createdAt)
|
|
9951
|
+
], options.all ? "no change requests" : "nothing waiting for review");
|
|
9952
|
+
});
|
|
9953
|
+
});
|
|
9954
|
+
changes.command("show <id>").description("show one change request and its reviews (never its value)").option("--org <slug>", "organization slug").option("--json", "print the raw API response").action(async (id, options) => {
|
|
9955
|
+
const ctx = buildContext();
|
|
9956
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
9957
|
+
const { changeRequest, reviews } = await ctx.client.getChangeRequest(ref.id, id);
|
|
9958
|
+
emit(options, {
|
|
9959
|
+
changeRequest,
|
|
9960
|
+
reviews
|
|
9961
|
+
}, () => {
|
|
9962
|
+
printFields([
|
|
9963
|
+
["id", changeRequest.id],
|
|
9964
|
+
["change", describeChange(changeRequest.kind, changeRequest.secretName, changeRequest.restoreFromVersion)],
|
|
9965
|
+
["environment", changeRequest.environmentId],
|
|
9966
|
+
["status", changeRequest.status],
|
|
9967
|
+
["base version", changeRequest.baseVersion ?? "(new secret)"],
|
|
9968
|
+
["approvals needed", changeRequest.requiredApprovals],
|
|
9969
|
+
["proposed by", `${changeRequest.requestedByType}:${changeRequest.requestedById}`],
|
|
9970
|
+
["note", changeRequest.note ?? "-"]
|
|
9971
|
+
]);
|
|
9972
|
+
section("reviews");
|
|
9973
|
+
printTable(reviews, [
|
|
9974
|
+
col("decision", (r) => r.decision),
|
|
9975
|
+
col("reviewer", (r) => r.reviewerId ?? "(removed)"),
|
|
9976
|
+
col("comment", (r) => r.comment ?? "-"),
|
|
9977
|
+
col("when", (r) => r.createdAt)
|
|
9978
|
+
], "no reviews yet");
|
|
9979
|
+
});
|
|
9980
|
+
});
|
|
9981
|
+
changes.command("approve <id>").description("approve a queued change (applies it once it has enough approvals)").option("--org <slug>", "organization slug").option("-m, --comment <text>", "note recorded with your approval").option("--json", "print the raw API response").action(async (id, options) => {
|
|
9982
|
+
const ctx = buildContext();
|
|
9983
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
9984
|
+
const result = await ctx.client.approveChangeRequest(ref.id, id, options.comment);
|
|
9985
|
+
emit(options, result, () => {
|
|
9986
|
+
if (result.status === "applied") console.error(`approved and applied${result.version ? ` as v${result.version}` : ""}`);
|
|
9987
|
+
else if (result.status === "superseded") console.error(`not applied: ${result.reason ?? "the secret has changed since"}`);
|
|
9988
|
+
else console.error(`approved — ${result.approvals ?? 0}/${result.required ?? "?"} approvals`);
|
|
9989
|
+
});
|
|
9990
|
+
});
|
|
9991
|
+
changes.command("reject <id>").description("reject a queued change").option("--org <slug>", "organization slug").option("-m, --comment <text>", "note recorded with your rejection").option("--json", "print the raw API response").action(async (id, options) => {
|
|
9992
|
+
const ctx = buildContext();
|
|
9993
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
9994
|
+
emit(options, await ctx.client.rejectChangeRequest(ref.id, id, options.comment), () => console.error("rejected"));
|
|
9995
|
+
});
|
|
9996
|
+
changes.command("withdraw <id>").description("take your own queued change off the review queue").option("--org <slug>", "organization slug").option("--json", "print the raw API response").action(async (id, options) => {
|
|
9997
|
+
const ctx = buildContext();
|
|
9998
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
9999
|
+
emit(options, await ctx.client.withdrawChangeRequest(ref.id, id), () => console.error("withdrawn"));
|
|
10000
|
+
});
|
|
10001
|
+
const protection = changes.command("protection").description("require review before an environment's secrets change");
|
|
10002
|
+
protection.command("show", { isDefault: true }).description("show whether an environment is protected").option("--org <slug>", "organization slug").option("--app <slug>", "application slug").option("--group <slug>", "group slug (instead of --app)").option("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
|
|
10003
|
+
const ctx = buildContext();
|
|
10004
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
10005
|
+
const result = await ctx.client.getEnvironmentProtection(orgId, envId);
|
|
10006
|
+
emit(options, result, () => {
|
|
10007
|
+
if (!result.protection) {
|
|
10008
|
+
console.log(`${label}: unprotected — writes land immediately`);
|
|
10009
|
+
return;
|
|
10010
|
+
}
|
|
10011
|
+
printFields([
|
|
10012
|
+
["environment", label],
|
|
10013
|
+
["approvals required", result.protection.requiredApprovals],
|
|
10014
|
+
["self-approval", result.protection.allowSelfApproval ? "allowed" : "not allowed"],
|
|
10015
|
+
["waiting for review", result.openChangeRequests]
|
|
10016
|
+
]);
|
|
10017
|
+
});
|
|
10018
|
+
});
|
|
10019
|
+
protection.command("on").description("require approval before secrets in this environment change").option("--org <slug>", "organization slug").option("--app <slug>", "application slug").option("--group <slug>", "group slug (instead of --app)").option("--env <slug>", "environment slug").option("--approvals <n>", "approvals a change needs", "1").option("--allow-self-approval", "let the proposer's own approval count").option("--json", "print the raw API response").action(async (options) => {
|
|
10020
|
+
const ctx = buildContext();
|
|
10021
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
10022
|
+
const requiredApprovals = Number(options.approvals);
|
|
10023
|
+
if (!Number.isInteger(requiredApprovals) || requiredApprovals < 1) fail("--approvals must be a positive whole number");
|
|
10024
|
+
emit(options, await ctx.client.setEnvironmentProtection(orgId, envId, {
|
|
10025
|
+
requiredApprovals,
|
|
10026
|
+
allowSelfApproval: options.allowSelfApproval ?? false
|
|
10027
|
+
}), () => console.error(`${label} now requires ${requiredApprovals} approval${requiredApprovals === 1 ? "" : "s"} before a secret changes`));
|
|
10028
|
+
});
|
|
10029
|
+
protection.command("off").description("stop requiring approval (queued changes stay queued)").option("--org <slug>", "organization slug").option("--app <slug>", "application slug").option("--group <slug>", "group slug (instead of --app)").option("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
|
|
10030
|
+
const ctx = buildContext();
|
|
10031
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
10032
|
+
emit(options, await ctx.client.deleteEnvironmentProtection(orgId, envId), () => console.error(`${label} no longer requires approval — writes land immediately`));
|
|
10033
|
+
});
|
|
10034
|
+
}
|
|
10035
|
+
function parseStatus(raw) {
|
|
10036
|
+
if (!raw) return void 0;
|
|
10037
|
+
if (!CHANGE_REQUEST_STATUSES.includes(raw)) fail(`unknown status "${raw}" — one of ${CHANGE_REQUEST_STATUSES.join(", ")}`);
|
|
10038
|
+
return raw;
|
|
10039
|
+
}
|
|
10040
|
+
/**
|
|
10041
|
+
* Report a write that was queued instead of performed. Every write path calls
|
|
10042
|
+
* this: a CLI that printed "set FOO" for a change nobody has approved would be
|
|
10043
|
+
* telling the operator the deploy is ready when it is not.
|
|
10044
|
+
*/
|
|
10045
|
+
function reportQueued(cr) {
|
|
10046
|
+
console.error(`queued for review: ${describeChange(cr.kind, cr.secretName, cr.restoreFromVersion)} — needs ${cr.requiredApprovals} approval${cr.requiredApprovals === 1 ? "" : "s"} (${cr.id})`);
|
|
10047
|
+
}
|
|
9766
10048
|
/** Plugin id and integration id declared by `@seekrit/openclaw-plugin`. */
|
|
9767
10049
|
const PLUGIN_ID = "seekrit";
|
|
9768
10050
|
const PLUGIN_INTEGRATION_ID = "seekrit";
|
|
@@ -11599,7 +11881,7 @@ function collect$4(value, acc) {
|
|
|
11599
11881
|
* release-please-config.json), so the pin follows the crate without anyone
|
|
11600
11882
|
* remembering to move it.
|
|
11601
11883
|
*/
|
|
11602
|
-
const PROXY_VERSION = "0.12.
|
|
11884
|
+
const PROXY_VERSION = "0.12.2";
|
|
11603
11885
|
const BIN = "seekrit-proxy";
|
|
11604
11886
|
/**
|
|
11605
11887
|
* Host → Rust target triple.
|
|
@@ -11635,7 +11917,7 @@ function versionPrefix(version) {
|
|
|
11635
11917
|
return version.startsWith("v") ? version : `v${version}`;
|
|
11636
11918
|
}
|
|
11637
11919
|
function resolveVersion(explicit) {
|
|
11638
|
-
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.
|
|
11920
|
+
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.2";
|
|
11639
11921
|
}
|
|
11640
11922
|
function resolveBaseUrl(explicit) {
|
|
11641
11923
|
return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
|
|
@@ -11793,7 +12075,7 @@ const PLUGIN_BIN = "seekrit-nomad-secrets";
|
|
|
11793
12075
|
* entry in release-please-config.json), so the pin follows the crate without
|
|
11794
12076
|
* anyone remembering to move it — the same arrangement `PROXY_VERSION` has.
|
|
11795
12077
|
*/
|
|
11796
|
-
const NOMAD_PLUGIN_VERSION = "0.2.
|
|
12078
|
+
const NOMAD_PLUGIN_VERSION = "0.2.1";
|
|
11797
12079
|
/** Public artifact host. Mirrors GitHub Releases; public, no egress fees. */
|
|
11798
12080
|
const DEFAULT_BASE_URL = "https://nomad.seekrit.dev";
|
|
11799
12081
|
/** Where the plugin reads its node config, matching `config::DEFAULT_CONFIG_PATH`. */
|
|
@@ -11880,7 +12162,7 @@ async function fetchBytes(url) {
|
|
|
11880
12162
|
* let Nomad fingerprint a half-written executable.
|
|
11881
12163
|
*/
|
|
11882
12164
|
async function installPlugin(pluginDir, options = {}) {
|
|
11883
|
-
const version = options.version ?? "0.2.
|
|
12165
|
+
const version = options.version ?? "0.2.1";
|
|
11884
12166
|
const baseUrl = (options.baseUrl ?? "https://nomad.seekrit.dev").replace(/\/+$/, "");
|
|
11885
12167
|
const { target, exe } = detectTarget();
|
|
11886
12168
|
if (exe !== "") fail("Nomad secret provider plugins are not supported on Windows clients");
|
|
@@ -13585,10 +13867,10 @@ Next:
|
|
|
13585
13867
|
});
|
|
13586
13868
|
console.log(path);
|
|
13587
13869
|
const { target } = detectTarget();
|
|
13588
|
-
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.12.
|
|
13870
|
+
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.12.2")} (${target})\n`);
|
|
13589
13871
|
});
|
|
13590
13872
|
proxy.command("where").description("show which binary `seekrit proxy run` would use, without fetching it").option("--proxy-version <version>", `version to report (default: ${PROXY_VERSION})`).option("--json", "machine-readable output").action((options) => {
|
|
13591
|
-
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.
|
|
13873
|
+
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.2";
|
|
13592
13874
|
const override = process.env.SEEKRIT_PROXY_BIN;
|
|
13593
13875
|
const { target, exe } = detectTarget();
|
|
13594
13876
|
const path = override ?? proxyBinaryPath(version, target, exe);
|
|
@@ -13647,7 +13929,7 @@ Next:
|
|
|
13647
13929
|
process.stdout.write(renderComposeSnippet(plan, {
|
|
13648
13930
|
service: options.service,
|
|
13649
13931
|
workload: options.workload,
|
|
13650
|
-
image: options.image ?? `seekritdev/proxy:0.12.
|
|
13932
|
+
image: options.image ?? `seekritdev/proxy:0.12.2`,
|
|
13651
13933
|
publish: Boolean(options.publish)
|
|
13652
13934
|
}));
|
|
13653
13935
|
});
|
|
@@ -15072,6 +15354,14 @@ function parseVersion(raw) {
|
|
|
15072
15354
|
* line is the whole invocation for CI and for agents. `resolveEnvTarget` fills
|
|
15073
15355
|
* it in from the token, and still insists on it for a user session.
|
|
15074
15356
|
*/
|
|
15357
|
+
/**
|
|
15358
|
+
* Attach `--note` to a command that *writes*. In a protected environment the
|
|
15359
|
+
* write becomes a change request, and the note is what tells the reviewer why —
|
|
15360
|
+
* so it is offered on every write path, and silently ignored everywhere else.
|
|
15361
|
+
*/
|
|
15362
|
+
function withNote(cmd) {
|
|
15363
|
+
return cmd.option("-m, --note <text>", "why (shown to reviewers if the change needs approval)");
|
|
15364
|
+
}
|
|
15075
15365
|
function withTarget(cmd) {
|
|
15076
15366
|
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").option("--branch <slug>", "operate on a branch of --env").option("--env <slug>", "environment slug (a service token infers its own)");
|
|
15077
15367
|
}
|
|
@@ -15102,7 +15392,7 @@ withTarget(secrets.command("get <name>").description("decrypt and print one secr
|
|
|
15102
15392
|
process.stdout.write(options.pretty ? prettyJsonOrRaw(value) : value);
|
|
15103
15393
|
if (process.stdout.isTTY) process.stdout.write("\n");
|
|
15104
15394
|
});
|
|
15105
|
-
withTarget(secrets.command("set <name> [value]").description("encrypt and store a secret (reads stdin when value is omitted or '-')").option("--file <path>", "read the value from a file — the way to store a JSON credential or a PEM key without fighting shell quoting")).action(async (name, value, options) => {
|
|
15395
|
+
withNote(withTarget(secrets.command("set <name> [value]").description("encrypt and store a secret (reads stdin when value is omitted or '-')").option("--file <path>", "read the value from a file — the way to store a JSON credential or a PEM key without fighting shell quoting"))).action(async (name, value, options) => {
|
|
15106
15396
|
if (options.file !== void 0 && value !== void 0) fail("pass a value or --file, not both");
|
|
15107
15397
|
const ctx = buildContext();
|
|
15108
15398
|
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
@@ -15112,10 +15402,11 @@ withTarget(secrets.command("set <name> [value]").description("encrypt and store
|
|
|
15112
15402
|
plaintext = readFileSync(options.file, "utf8").replace(/\n$/, "");
|
|
15113
15403
|
} else if (value === void 0 || value === "-") plaintext = (await readStdin()).replace(/\n$/, "");
|
|
15114
15404
|
else plaintext = value;
|
|
15115
|
-
await encryptAndSetSecret(ctx, orgId, envId, name, plaintext);
|
|
15116
|
-
|
|
15405
|
+
const queued = await encryptAndSetSecret(ctx, orgId, envId, name, plaintext, options.note);
|
|
15406
|
+
if (queued) reportQueued(queued);
|
|
15407
|
+
else console.error(`${name} saved`);
|
|
15117
15408
|
});
|
|
15118
|
-
withTarget(secrets.command("import [file]").description("bulk-import secrets from a .env file (default .env; '-' reads stdin)").option("--dry-run", "list the variable names that would be imported, without writing")).action(async (file, options) => {
|
|
15409
|
+
withNote(withTarget(secrets.command("import [file]").description("bulk-import secrets from a .env file (default .env; '-' reads stdin)").option("--dry-run", "list the variable names that would be imported, without writing"))).action(async (file, options) => {
|
|
15119
15410
|
const source = file ?? ".env";
|
|
15120
15411
|
let content;
|
|
15121
15412
|
if (source === "-") content = await readStdin();
|
|
@@ -15137,8 +15428,11 @@ withTarget(secrets.command("import [file]").description("bulk-import secrets fro
|
|
|
15137
15428
|
for (const name of names.sort()) console.error(` ${name}\t${existing.has(name) ? "update" : "new"}`);
|
|
15138
15429
|
return;
|
|
15139
15430
|
}
|
|
15140
|
-
const { created, updated } = await importSecrets(ctx, orgId, envId, entries);
|
|
15141
|
-
|
|
15431
|
+
const { created, updated, queued } = await importSecrets(ctx, orgId, envId, entries, options.note);
|
|
15432
|
+
if (queued.length > 0) {
|
|
15433
|
+
console.error(`${queued.length} change(s) queued for review in ${label} — nothing has changed yet`);
|
|
15434
|
+
for (const cr of queued) reportQueued(cr);
|
|
15435
|
+
} else console.error(`imported ${created.length + updated.length} secret(s) into ${label} (${created.length} new, ${updated.length} updated)`);
|
|
15142
15436
|
});
|
|
15143
15437
|
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").option("--json", "print the raw API response")).action(async (name, options) => {
|
|
15144
15438
|
const ctx = buildContext();
|
|
@@ -15154,18 +15448,20 @@ withTarget(secrets.command("history <name>").description("list a secret's versio
|
|
|
15154
15448
|
col("note", (v) => [v.version === currentVersion ? "current" : null, v.restoredFromVersion === null ? null : `restored from v${v.restoredFromVersion}`].filter(Boolean).join(", "))
|
|
15155
15449
|
], `no versions of ${name}`));
|
|
15156
15450
|
});
|
|
15157
|
-
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) => {
|
|
15451
|
+
withNote(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) => {
|
|
15158
15452
|
const ctx = buildContext();
|
|
15159
15453
|
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
15160
|
-
const
|
|
15161
|
-
|
|
15454
|
+
const result = await ctx.client.restoreSecret(orgId, envId, name, parseVersion(version), options.note);
|
|
15455
|
+
if (result.queued) reportQueued(result.changeRequest);
|
|
15456
|
+
else console.error(`${name} restored from v${result.restoredFrom} — now v${result.secret.version}`);
|
|
15162
15457
|
});
|
|
15163
|
-
withTarget(secrets.command("rm <name>").description("delete a secret and its whole version history").option("-y, --yes", "skip the confirmation")).action(async (name, options) => {
|
|
15458
|
+
withNote(withTarget(secrets.command("rm <name>").description("delete a secret and its whole version history").option("-y, --yes", "skip the confirmation"))).action(async (name, options) => {
|
|
15164
15459
|
const ctx = buildContext();
|
|
15165
15460
|
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
15166
15461
|
await confirmDestructive(options.yes, `Delete ${name} from ${label}, including every earlier version? This cannot be undone.`);
|
|
15167
|
-
await ctx.client.deleteSecret(orgId, envId, name);
|
|
15168
|
-
|
|
15462
|
+
const removed = await ctx.client.deleteSecret(orgId, envId, name, options.note);
|
|
15463
|
+
if (removed.queued) reportQueued(removed.changeRequest);
|
|
15464
|
+
else console.error(`${name} deleted`);
|
|
15169
15465
|
});
|
|
15170
15466
|
/**
|
|
15171
15467
|
* Open the last-known-good cache for this request, or `undefined` when it is
|
|
@@ -15514,6 +15810,7 @@ token.command("delete <tokenId>").description("permanently delete a revoked serv
|
|
|
15514
15810
|
console.error(`${tokenId} deleted`);
|
|
15515
15811
|
});
|
|
15516
15812
|
registerBranchCommands(program);
|
|
15813
|
+
registerChangeCommands(program);
|
|
15517
15814
|
registerPgCommands(program);
|
|
15518
15815
|
registerMysqlCommands(program);
|
|
15519
15816
|
registerRedisCommands(program);
|
|
@@ -15532,7 +15829,7 @@ registerKmsCommands(program);
|
|
|
15532
15829
|
registerRotationCommands(program);
|
|
15533
15830
|
registerRecoveryCommands(program);
|
|
15534
15831
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
15535
|
-
const { runMcpServer } = await import("./mcp-
|
|
15832
|
+
const { runMcpServer } = await import("./mcp-ZYu1Q6Ft.js");
|
|
15536
15833
|
await runMcpServer();
|
|
15537
15834
|
});
|
|
15538
15835
|
registerAuditCommands(program);
|
|
@@ -15547,4 +15844,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
15547
15844
|
fail(describeError(err));
|
|
15548
15845
|
});
|
|
15549
15846
|
//#endregion
|
|
15550
|
-
export {
|
|
15847
|
+
export { signatureKeyRef as A, generateDek as B, createServiceToken as C, importSigningKey as D, generateSigningKeyMaterial as E, generateEncryptKeyMaterial as F, parseBranchTtl as H, kmsBlobKeyRef as I, kmsDecrypt as L, generatePostgresCredential as M, generateMysqlCredential as N, importVerifyingKey as O, generateDataKey as P, kmsEncrypt as R, version as S, parseServiceToken as T, toBase64 as V, getDek as _, kmsResolveRecipient as a, setFailThrows as b, fetchDecryptedSecrets as c, resolveApp as d, resolveAppEnv as f, resolveOrg as g, resolveGroup as h, kmsResolveKey as i, verifyMessage as j, signMessage as k, fetchDecryptedVersion as l, resolveEnvTarget as m, kmsCallerIdentity as n, syncGrantsForGroupLink as o, resolveBranch as p, kmsRecoverMaterial as r, encryptAndSetSecret as s, ensureM2mAdminToken as t, materializeEnv as u, isTokenAuth as v, isServiceToken as w, writeProjectConfig as x, tryBuildContext as y, wrapDek as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as signatureKeyRef, B as generateDek, C as createServiceToken, D as importSigningKey, E as generateSigningKeyMaterial, F as generateEncryptKeyMaterial, H as parseBranchTtl, I as kmsBlobKeyRef, L as kmsDecrypt, M as generatePostgresCredential, N as generateMysqlCredential, O as importVerifyingKey, P as generateDataKey, R as kmsEncrypt, S as version, T as parseServiceToken, V as toBase64, _ as getDek, a as kmsResolveRecipient, b as setFailThrows, c as fetchDecryptedSecrets, d as resolveApp, f as resolveAppEnv, g as resolveOrg, h as resolveGroup, i as kmsResolveKey, j as verifyMessage, k as signMessage, l as fetchDecryptedVersion, m as resolveEnvTarget, n as kmsCallerIdentity, o as syncGrantsForGroupLink, p as resolveBranch, r as kmsRecoverMaterial, s as encryptAndSetSecret, t as ensureM2mAdminToken, u as materializeEnv, v as isTokenAuth, w as isServiceToken, x as writeProjectConfig, y as tryBuildContext, z as wrapDek } 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";
|
|
@@ -621,9 +621,11 @@ async function runMcpServer(options = {}) {
|
|
|
621
621
|
org,
|
|
622
622
|
group
|
|
623
623
|
});
|
|
624
|
+
const syncGrants = await syncGrantsForGroupLink(ctx, target.orgId, target.envId, target.envSlug, g.id);
|
|
624
625
|
return (await ctx.client.linkEnvGroup(target.orgId, target.envId, {
|
|
625
626
|
groupId: g.id,
|
|
626
|
-
position
|
|
627
|
+
position,
|
|
628
|
+
syncGrants
|
|
627
629
|
})).group;
|
|
628
630
|
});
|
|
629
631
|
tool("uncompose_group", "Remove a composed group from an application environment.", { idempotent: true }, {
|
|
@@ -645,16 +647,23 @@ async function runMcpServer(options = {}) {
|
|
|
645
647
|
await ctx.client.unlinkEnvGroup(target.orgId, target.envId, g.id);
|
|
646
648
|
return { ok: true };
|
|
647
649
|
});
|
|
648
|
-
tool("set_secret", "Encrypt a value locally and store it in an environment. A value may reference another secret as ${OTHER_SECRET}: the reference is stored literally and expanded whenever the secret is read, so it tracks the referenced value. Write $${OTHER_SECRET} for a literal.", { idempotent: false }, {
|
|
650
|
+
tool("set_secret", "Encrypt a value locally and store it in an environment. A value may reference another secret as ${OTHER_SECRET}: the reference is stored literally and expanded whenever the secret is read, so it tracks the referenced value. Write $${OTHER_SECRET} for a literal. If the environment requires approval the write is QUEUED, not stored: the result carries queued:true and a changeRequestId, and the secret keeps its old value until a human approves.", { idempotent: false }, {
|
|
649
651
|
...targetShape,
|
|
650
652
|
name: z.string(),
|
|
651
|
-
value: z.string()
|
|
653
|
+
value: z.string(),
|
|
654
|
+
note: z.string().optional()
|
|
652
655
|
}, async (o) => {
|
|
653
656
|
const ctx = getCtx();
|
|
654
657
|
ensureDecryptable(ctx);
|
|
655
658
|
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
656
|
-
await encryptAndSetSecret(ctx, orgId, envId, o.name, o.value);
|
|
657
|
-
return {
|
|
659
|
+
const queued = await encryptAndSetSecret(ctx, orgId, envId, o.name, o.value, o.note);
|
|
660
|
+
return queued ? {
|
|
661
|
+
ok: true,
|
|
662
|
+
queued: true,
|
|
663
|
+
name: o.name,
|
|
664
|
+
changeRequestId: queued.id,
|
|
665
|
+
requiredApprovals: queued.requiredApprovals
|
|
666
|
+
} : {
|
|
658
667
|
ok: true,
|
|
659
668
|
name: o.name
|
|
660
669
|
};
|
|
@@ -717,26 +726,38 @@ async function runMcpServer(options = {}) {
|
|
|
717
726
|
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.", { idempotent: false }, {
|
|
718
727
|
...targetShape,
|
|
719
728
|
name: z.string(),
|
|
720
|
-
version: z.number().int().positive()
|
|
729
|
+
version: z.number().int().positive(),
|
|
730
|
+
note: z.string().optional()
|
|
721
731
|
}, async (o) => {
|
|
722
732
|
const ctx = getCtx();
|
|
723
733
|
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
724
|
-
const
|
|
725
|
-
return {
|
|
734
|
+
const result = await ctx.client.restoreSecret(orgId, envId, o.name, o.version, o.note);
|
|
735
|
+
return result.queued ? {
|
|
736
|
+
ok: true,
|
|
737
|
+
queued: true,
|
|
738
|
+
name: o.name,
|
|
739
|
+
changeRequestId: result.changeRequest.id
|
|
740
|
+
} : {
|
|
726
741
|
ok: true,
|
|
727
742
|
name: o.name,
|
|
728
|
-
restoredFrom,
|
|
729
|
-
version: secret.version
|
|
743
|
+
restoredFrom: result.restoredFrom,
|
|
744
|
+
version: result.secret.version
|
|
730
745
|
};
|
|
731
746
|
});
|
|
732
747
|
tool("delete_secret", "Delete a secret from an environment.", destructive, {
|
|
733
748
|
...targetShape,
|
|
734
|
-
name: z.string()
|
|
749
|
+
name: z.string(),
|
|
750
|
+
note: z.string().optional()
|
|
735
751
|
}, async (o) => {
|
|
736
752
|
const ctx = getCtx();
|
|
737
753
|
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
738
|
-
await ctx.client.deleteSecret(orgId, envId, o.name);
|
|
739
|
-
return {
|
|
754
|
+
const removed = await ctx.client.deleteSecret(orgId, envId, o.name, o.note);
|
|
755
|
+
return removed.queued ? {
|
|
756
|
+
ok: true,
|
|
757
|
+
queued: true,
|
|
758
|
+
name: o.name,
|
|
759
|
+
changeRequestId: removed.changeRequest.id
|
|
760
|
+
} : {
|
|
740
761
|
ok: true,
|
|
741
762
|
name: o.name
|
|
742
763
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
26
26
|
"commander": "^15.0.0",
|
|
27
|
-
"zod": "^4.6.
|
|
27
|
+
"zod": "^4.6.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.
|
|
30
|
+
"@types/node": "^26.6.1",
|
|
31
31
|
"tsdown": "^0.23.0",
|
|
32
|
-
"vitest": "^
|
|
32
|
+
"vitest": "^5.0.1",
|
|
33
33
|
"@seekrit/api-client": "0.0.1",
|
|
34
34
|
"@seekrit/core": "0.0.1",
|
|
35
35
|
"@seekrit/crypto": "0.0.1"
|