@seekrit/cli 0.31.0 → 0.33.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
@@ -16,6 +16,12 @@ const ENTITLEMENT_KEYS = Object.keys({
16
16
  description: "Client-side managed keys for application-layer encryption and signing.",
17
17
  default: true
18
18
  },
19
+ "feature.honey_tokens": {
20
+ kind: "feature",
21
+ label: "Honey tokens",
22
+ description: "Decoy credentials that alert the moment anyone tries to use them.",
23
+ default: true
24
+ },
19
25
  "feature.leases": {
20
26
  kind: "feature",
21
27
  label: "Temporary access",
@@ -122,7 +128,7 @@ const PLAN_FAMILIES = {
122
128
  id: "free",
123
129
  name: "Free",
124
130
  description: "Get started with the essentials.",
125
- current: 1,
131
+ current: 3,
126
132
  hidden: false
127
133
  },
128
134
  team: {
@@ -1119,6 +1125,9 @@ const AUDIT_ACTIONS = [
1119
1125
  "token.created",
1120
1126
  "token.revoked",
1121
1127
  "token.deleted",
1128
+ "honey_token.created",
1129
+ "honey_token.deleted",
1130
+ "honey_token.tripped",
1122
1131
  "cli_session.approved",
1123
1132
  "cli_session.denied",
1124
1133
  "cli_session.revoked",
@@ -1177,7 +1186,8 @@ const NOTIFICATION_TYPES = [
1177
1186
  "org_welcome",
1178
1187
  "token_expiring",
1179
1188
  "lease_expired",
1180
- "sync_failed"
1189
+ "sync_failed",
1190
+ "honey_token_tripped"
1181
1191
  ];
1182
1192
  /** UI-facing copy + default for each notification type. */
1183
1193
  const NOTIFICATION_TYPE_META = {
@@ -1225,6 +1235,11 @@ const NOTIFICATION_TYPE_META = {
1225
1235
  label: "Third-party sync failed",
1226
1236
  description: "A sync to an external destination failed repeatedly and stopped retrying. The destination is now holding stale values.",
1227
1237
  defaultEnabled: true
1238
+ },
1239
+ honey_token_tripped: {
1240
+ label: "Honey token tripped",
1241
+ description: "Someone tried to use one of your organization's decoy credentials (throttled per token). Nothing legitimate holds one, so every trip is worth reading.",
1242
+ defaultEnabled: true
1228
1243
  }
1229
1244
  };
1230
1245
  //#endregion
@@ -1332,6 +1347,14 @@ z.object({
1332
1347
  environmentId: z.string().min(1).nullish(),
1333
1348
  expiresAt: z.iso.datetime().nullish()
1334
1349
  });
1350
+ z.object({
1351
+ name: nameSchema,
1352
+ tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
1353
+ /** SHA-256 hash (base64url) of the full token string. */
1354
+ tokenHash: z.string().min(1),
1355
+ /** Where the decoy was planted, as a reminder for whoever reads the alert. */
1356
+ placement: z.string().max(200).nullish()
1357
+ });
1335
1358
  const kmsKeyPurposeSchema = z.enum(["encrypt", "sign"]);
1336
1359
  const kmsKeySpecSchema = z.enum(["aes-256-gcm", "ecdsa-p256"]);
1337
1360
  /** A wrapped key grant supplied by the client (server never sees plaintext material). */
@@ -2684,6 +2707,14 @@ async function createServiceToken() {
2684
2707
  publicKeyJwk
2685
2708
  };
2686
2709
  }
2710
+ async function createHoneyToken() {
2711
+ const { token, tokenId, tokenHash } = await createServiceToken();
2712
+ return {
2713
+ token,
2714
+ tokenId,
2715
+ tokenHash
2716
+ };
2717
+ }
2687
2718
  async function parseServiceToken(token) {
2688
2719
  const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
2689
2720
  if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
@@ -2720,7 +2751,7 @@ function isCliSessionToken(value) {
2720
2751
  }
2721
2752
  //#endregion
2722
2753
  //#region package.json
2723
- var version = "0.31.0";
2754
+ var version = "0.33.0";
2724
2755
  //#endregion
2725
2756
  //#region ../../packages/api-client/src/index.ts
2726
2757
  var SeekritApiError = class extends Error {
@@ -3032,6 +3063,16 @@ var SeekritClient = class {
3032
3063
  deleteToken(orgId, tokenId) {
3033
3064
  return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}/permanent`);
3034
3065
  }
3066
+ listHoneyTokens(orgId) {
3067
+ return this.request("GET", `/v1/orgs/${orgId}/honey-tokens`);
3068
+ }
3069
+ createHoneyToken(orgId, input) {
3070
+ return this.request("POST", `/v1/orgs/${orgId}/honey-tokens`, input);
3071
+ }
3072
+ /** Delete a decoy outright — there is no access to revoke first. */
3073
+ deleteHoneyToken(orgId, honeyTokenId) {
3074
+ return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
3075
+ }
3035
3076
  /** Keys the caller can see: all org keys for admins, granted keys otherwise. */
3036
3077
  listKmsKeys(orgId) {
3037
3078
  return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
@@ -5233,6 +5274,52 @@ function registerGroupCommands(program) {
5233
5274
  });
5234
5275
  }
5235
5276
  //#endregion
5277
+ //#region src/honey.ts
5278
+ /**
5279
+ * Honey tokens — decoy credentials that unlock nothing and alert when used.
5280
+ *
5281
+ * Minted client-side like real tokens (the API only ever sees a hash), printed
5282
+ * once, and then planted wherever a thief would rummage. The CLI is the natural
5283
+ * home for this: `seekrit honey-token create` piped straight into the file,
5284
+ * commit, config, or CI variable you want watched.
5285
+ */
5286
+ function registerHoneyTokenCommands(program) {
5287
+ const honey = program.command("honey-token").description("plant decoy credentials that alert when anyone tries to use them");
5288
+ 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) => {
5289
+ const ctx = buildContext();
5290
+ const orgRef = await resolveOrg(ctx, options.org);
5291
+ const created = await createHoneyToken();
5292
+ await ctx.client.createHoneyToken(orgRef.id, {
5293
+ name: options.name,
5294
+ tokenId: created.tokenId,
5295
+ tokenHash: created.tokenHash,
5296
+ placement: options.placement ?? null
5297
+ });
5298
+ 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.");
5299
+ console.log(created.token);
5300
+ });
5301
+ 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) => {
5302
+ const ctx = buildContext();
5303
+ const orgRef = await resolveOrg(ctx, options.org);
5304
+ const { honeyTokens } = await ctx.client.listHoneyTokens(orgRef.id);
5305
+ emit(options, { honeyTokens }, () => printTable(honeyTokens, [
5306
+ col("name", (t) => t.name),
5307
+ col("status", (t) => t.tripCount > 0 ? `TRIPPED ${t.tripCount}×` : "untouched"),
5308
+ col("planted in", (t) => t.placement ?? "—"),
5309
+ col("last tripped", (t) => t.lastTrippedAt ?? "never"),
5310
+ col("last source", (t) => t.lastTripIp ?? "—"),
5311
+ col("id", (t) => t.id)
5312
+ ], "no decoys planted — create one with `seekrit honey-token create`"));
5313
+ });
5314
+ 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) => {
5315
+ const ctx = buildContext();
5316
+ const orgRef = await resolveOrg(ctx, options.org);
5317
+ await confirmDestructive(options.yes, `Delete ${honeyTokenId}? Wherever you planted it goes back to being unwatched — pull the bait too.`);
5318
+ await ctx.client.deleteHoneyToken(orgRef.id, honeyTokenId);
5319
+ console.error(`${honeyTokenId} deleted`);
5320
+ });
5321
+ }
5322
+ //#endregion
5236
5323
  //#region src/logsink.ts
5237
5324
  /** Collect repeated `--header Name: value` flags into a map. */
5238
5325
  function collectHeader(value, acc = {}) {
@@ -7307,6 +7394,7 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
7307
7394
  console.log(formatSecrets(values, options.format));
7308
7395
  });
7309
7396
  registerAccessCommands(program);
7397
+ registerHoneyTokenCommands(program);
7310
7398
  const token = program.command("token").description("manage service tokens (CI, docker, agents)");
7311
7399
  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) => {
7312
7400
  const ctx = buildContext();
@@ -7395,7 +7483,7 @@ registerMongoCommands(program);
7395
7483
  registerKmsCommands(program);
7396
7484
  registerRecoveryCommands(program);
7397
7485
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
7398
- const { runMcpServer } = await import("./mcp-DLplPOvz.js");
7486
+ const { runMcpServer } = await import("./mcp-DR-zla_u.js");
7399
7487
  await runMcpServer();
7400
7488
  });
7401
7489
  registerAuditCommands(program);
@@ -26,6 +26,16 @@ function errText(err) {
26
26
  isError: true
27
27
  };
28
28
  }
29
+ /** Read-only and safe to repeat — every list/inspect tool. */
30
+ const ro = {
31
+ readOnly: true,
32
+ idempotent: true
33
+ };
34
+ /** Removes or overwrites something; repeating it lands in the same state. */
35
+ const destructive = {
36
+ destructive: true,
37
+ idempotent: true
38
+ };
29
39
  /**
30
40
  * Short primer surfaced as the MCP server `instructions`. Most clients show
31
41
  * this to the model on connect, so it has to orient an agent that lands here
@@ -194,10 +204,17 @@ async function runMcpServer(options = {}) {
194
204
  version: options.version ?? version
195
205
  }, { instructions: serverInstructions() });
196
206
  /** Register a tool whose handler returns data (serialized) or throws (→ isError). */
197
- const tool = (name, description, shape, handler) => {
207
+ const tool = (name, description, hints, shape, handler) => {
198
208
  server.registerTool(name, {
199
209
  description,
200
- inputSchema: shape
210
+ inputSchema: shape,
211
+ annotations: {
212
+ title: name,
213
+ readOnlyHint: hints.readOnly ?? false,
214
+ destructiveHint: hints.destructive ?? false,
215
+ idempotentHint: hints.idempotent ?? false,
216
+ openWorldHint: true
217
+ }
201
218
  }, (async (args) => {
202
219
  try {
203
220
  return jsonText(await handler(args));
@@ -215,7 +232,7 @@ async function runMcpServer(options = {}) {
215
232
  openWorldHint: false
216
233
  }
217
234
  }, async () => jsonText(getStartedText()));
218
- tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", {}, async () => {
235
+ tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", ro, {}, async () => {
219
236
  const ctx = getCtx();
220
237
  if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
221
238
  const { tokenId } = await parseServiceToken(ctx.auth.token);
@@ -236,13 +253,13 @@ async function runMcpServer(options = {}) {
236
253
  ...await ctx.client.me()
237
254
  };
238
255
  });
239
- tool("list_orgs", "List organizations the caller can access.", {}, async () => (await getCtx().client.listOrgs()).orgs);
240
- tool("list_apps", "List applications in an organization.", { org: z.string().optional() }, async ({ org }) => {
256
+ tool("list_orgs", "List organizations the caller can access.", ro, {}, async () => (await getCtx().client.listOrgs()).orgs);
257
+ tool("list_apps", "List applications in an organization.", ro, { org: z.string().optional() }, async ({ org }) => {
241
258
  const ctx = getCtx();
242
259
  const orgRef = await resolveOrg(ctx, org);
243
260
  return (await ctx.client.listApps(orgRef.id)).apps;
244
261
  });
245
- tool("list_envs", "List environments of an application.", {
262
+ tool("list_envs", "List environments of an application.", ro, {
246
263
  org: z.string().optional(),
247
264
  app: z.string()
248
265
  }, async ({ org, app }) => {
@@ -253,7 +270,7 @@ async function runMcpServer(options = {}) {
253
270
  if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
254
271
  return (await ctx.client.listEnvs(orgRef.id, appRow.id)).environments;
255
272
  });
256
- tool("list_branches", "List ephemeral branch configs in an application (optionally just one environment's).", {
273
+ tool("list_branches", "List ephemeral branch configs in an application (optionally just one environment's).", ro, {
257
274
  org: z.string().optional(),
258
275
  app: z.string(),
259
276
  env: z.string().optional()
@@ -271,12 +288,12 @@ async function runMcpServer(options = {}) {
271
288
  });
272
289
  return (await ctx.client.listBranches(parent.orgId, parent.envId)).branches;
273
290
  });
274
- tool("list_groups", "List shared groups (reusable secret bags) in an organization.", { org: z.string().optional() }, async ({ org }) => {
291
+ tool("list_groups", "List shared groups (reusable secret bags) in an organization.", ro, { org: z.string().optional() }, async ({ org }) => {
275
292
  const ctx = getCtx();
276
293
  const orgRef = await resolveOrg(ctx, org);
277
294
  return (await ctx.client.listGroups(orgRef.id)).groups;
278
295
  });
279
- tool("list_group_envs", "List a group's environments (per-slug value sets).", {
296
+ tool("list_group_envs", "List a group's environments (per-slug value sets).", ro, {
280
297
  org: z.string().optional(),
281
298
  group: z.string()
282
299
  }, async ({ org, group }) => {
@@ -287,7 +304,7 @@ async function runMcpServer(options = {}) {
287
304
  });
288
305
  return (await ctx.client.listGroupEnvs(g.orgId, g.id)).environments;
289
306
  });
290
- tool("list_env_groups", "List the groups composed into an application environment (precedence order).", {
307
+ tool("list_env_groups", "List the groups composed into an application environment (precedence order).", ro, {
291
308
  org: z.string().optional(),
292
309
  app: z.string(),
293
310
  env: z.string()
@@ -300,17 +317,17 @@ async function runMcpServer(options = {}) {
300
317
  });
301
318
  return (await ctx.client.listEnvGroups(target.orgId, target.envId)).groups;
302
319
  });
303
- tool("list_members", "List organization members and their public keys (for granting access).", { org: z.string().optional() }, async ({ org }) => {
320
+ tool("list_members", "List organization members and their public keys (for granting access).", ro, { org: z.string().optional() }, async ({ org }) => {
304
321
  const ctx = getCtx();
305
322
  const orgRef = await resolveOrg(ctx, org);
306
323
  return (await ctx.client.listMembers(orgRef.id)).members;
307
324
  });
308
- tool("kms_list_keys", "List managed KMS keys the caller can see (metadata only).", { org: z.string().optional() }, async ({ org }) => {
325
+ tool("kms_list_keys", "List managed KMS keys the caller can see (metadata only).", ro, { org: z.string().optional() }, async ({ org }) => {
309
326
  const ctx = getCtx();
310
327
  const orgRef = await resolveOrg(ctx, org);
311
328
  return (await ctx.client.listKmsKeys(orgRef.id)).keys;
312
329
  });
313
- tool("kms_create_key", "Create an org-scoped managed key. Material is generated locally and wrapped to each grantee (self plus any listed users/tokens); the server never sees it. Use the CLI for app/group-scoped keys.", {
330
+ tool("kms_create_key", "Create an org-scoped managed key. Material is generated locally and wrapped to each grantee (self plus any listed users/tokens); the server never sees it. Use the CLI for app/group-scoped keys.", { idempotent: false }, {
314
331
  org: z.string().optional(),
315
332
  name: z.string(),
316
333
  purpose: z.enum(["encrypt", "sign"]),
@@ -351,7 +368,7 @@ async function runMcpServer(options = {}) {
351
368
  });
352
369
  return key;
353
370
  });
354
- tool("kms_grant", "Grant a principal (user email or token id) use of a key's current version.", {
371
+ tool("kms_grant", "Grant a principal (user email or token id) use of a key's current version.", { idempotent: true }, {
355
372
  org: z.string().optional(),
356
373
  key: z.string(),
357
374
  user: z.string().optional(),
@@ -376,7 +393,7 @@ async function runMcpServer(options = {}) {
376
393
  key: k.name
377
394
  };
378
395
  });
379
- tool("kms_encrypt", "Encrypt a value under a managed encrypt key; returns a ce1 ciphertext blob. `context` (if given) is bound as AAD and must be supplied identically to decrypt.", {
396
+ tool("kms_encrypt", "Encrypt a value under a managed encrypt key; returns a ce1 ciphertext blob. `context` (if given) is bound as AAD and must be supplied identically to decrypt.", { readOnly: true }, {
380
397
  org: z.string().optional(),
381
398
  key: z.string(),
382
399
  plaintext: z.string(),
@@ -393,7 +410,7 @@ async function runMcpServer(options = {}) {
393
410
  version: currentVersion
394
411
  }, plaintext, context ?? "") };
395
412
  });
396
- tool("kms_decrypt", "Decrypt a ce1 blob. Supply the same `context` used to encrypt.", {
413
+ tool("kms_decrypt", "Decrypt a ce1 blob. Supply the same `context` used to encrypt.", ro, {
397
414
  org: z.string().optional(),
398
415
  key: z.string(),
399
416
  ciphertext: z.string(),
@@ -407,7 +424,7 @@ async function runMcpServer(options = {}) {
407
424
  const { material } = await kmsRecoverMaterial(ctx, orgRef.id, k.id, ref.version);
408
425
  return { plaintext: await kmsDecrypt(material, ciphertext, context ?? "") };
409
426
  });
410
- tool("kms_generate_data_key", "Generate a data key under a managed encrypt key (envelope encryption). Returns the plaintext key (base64) and its wrapped form to store.", {
427
+ tool("kms_generate_data_key", "Generate a data key under a managed encrypt key (envelope encryption). Returns the plaintext key (base64) and its wrapped form to store.", { readOnly: true }, {
411
428
  org: z.string().optional(),
412
429
  key: z.string()
413
430
  }, async ({ org, key }) => {
@@ -426,7 +443,7 @@ async function runMcpServer(options = {}) {
426
443
  wrapped: dk.wrapped
427
444
  };
428
445
  });
429
- tool("kms_sign", "Sign a message with a managed signing key; returns an sg1 signature blob.", {
446
+ tool("kms_sign", "Sign a message with a managed signing key; returns an sg1 signature blob.", { readOnly: true }, {
430
447
  org: z.string().optional(),
431
448
  key: z.string(),
432
449
  message: z.string()
@@ -442,7 +459,7 @@ async function runMcpServer(options = {}) {
442
459
  version: currentVersion
443
460
  }, message) };
444
461
  });
445
- tool("kms_verify", "Verify an sg1 signature over a message using a signing key's published public key (no grant needed).", {
462
+ tool("kms_verify", "Verify an sg1 signature over a message using a signing key's published public key (no grant needed).", ro, {
446
463
  org: z.string().optional(),
447
464
  key: z.string(),
448
465
  signature: z.string(),
@@ -457,7 +474,7 @@ async function runMcpServer(options = {}) {
457
474
  if (!pub) throw new Error(`no published public key for version ${ref.version}`);
458
475
  return { valid: await verifyMessage(await importVerifyingKey(pub), signature, message) };
459
476
  });
460
- tool("list_secrets", "List secret names + versions in an environment (never values).", targetShape, async (o) => {
477
+ tool("list_secrets", "List secret names + versions in an environment (never values).", ro, targetShape, async (o) => {
461
478
  const ctx = getCtx();
462
479
  const { orgId, envId } = await resolveTargetEnv(ctx, o);
463
480
  const { secrets } = await ctx.client.listSecrets(orgId, envId);
@@ -467,12 +484,12 @@ async function runMcpServer(options = {}) {
467
484
  updatedAt: s.updatedAt
468
485
  }));
469
486
  });
470
- tool("list_tokens", "List an organization's service tokens (never the secret token strings).", { org: z.string().optional() }, async ({ org }) => {
487
+ tool("list_tokens", "List an organization's service tokens (never the secret token strings).", ro, { org: z.string().optional() }, async ({ org }) => {
471
488
  const ctx = getCtx();
472
489
  const orgRef = await resolveOrg(ctx, org);
473
490
  return (await ctx.client.listTokens(orgRef.id)).tokens;
474
491
  });
475
- tool("audit", "Read the organization's audit trail (most recent first).", {
492
+ tool("audit", "Read the organization's audit trail (most recent first).", ro, {
476
493
  org: z.string().optional(),
477
494
  limit: z.number().int().min(1).max(200).optional(),
478
495
  action: z.string().optional().describe("filter by action, e.g. secret.updated")
@@ -484,14 +501,14 @@ async function runMcpServer(options = {}) {
484
501
  action
485
502
  })).entries;
486
503
  });
487
- tool("create_org", "Create an organization. Requires a user session — service tokens cannot own a Stytch org.", {
504
+ tool("create_org", "Create an organization. Requires a user session — service tokens cannot own a Stytch org.", { idempotent: false }, {
488
505
  name: z.string(),
489
506
  slug: z.string()
490
507
  }, async ({ name, slug }) => (await getCtx().client.createOrg({
491
508
  name,
492
509
  slug
493
510
  })).org);
494
- tool("create_app", "Create an application in an organization.", {
511
+ tool("create_app", "Create an application in an organization.", { idempotent: false }, {
495
512
  org: z.string().optional(),
496
513
  name: z.string(),
497
514
  slug: z.string()
@@ -503,7 +520,7 @@ async function runMcpServer(options = {}) {
503
520
  slug
504
521
  })).app;
505
522
  });
506
- tool("create_group", "Create a shared group (reusable secret bag) in an organization.", {
523
+ tool("create_group", "Create a shared group (reusable secret bag) in an organization.", { idempotent: false }, {
507
524
  org: z.string().optional(),
508
525
  name: z.string(),
509
526
  slug: z.string()
@@ -515,7 +532,7 @@ async function runMcpServer(options = {}) {
515
532
  slug
516
533
  })).group;
517
534
  });
518
- tool("create_env", "Create an application environment. Generates the data key locally and grants it to the caller.", {
535
+ tool("create_env", "Create an application environment. Generates the data key locally and grants it to the caller.", { idempotent: false }, {
519
536
  org: z.string().optional(),
520
537
  app: z.string(),
521
538
  name: z.string(),
@@ -533,7 +550,7 @@ async function runMcpServer(options = {}) {
533
550
  wrappedDek
534
551
  })).environment;
535
552
  });
536
- tool("create_branch", "Fork an environment into an ephemeral branch (a per-PR / preview config). The branch inherits the parent's secrets by layering at read time — nothing is copied or re-encrypted — and holds only the values you override on it. Generates the branch's data key locally, grants it to the caller, and shares it with the parent's other readers.", {
553
+ tool("create_branch", "Fork an environment into an ephemeral branch (a per-PR / preview config). The branch inherits the parent's secrets by layering at read time — nothing is copied or re-encrypted — and holds only the values you override on it. Generates the branch's data key locally, grants it to the caller, and shares it with the parent's other readers.", { idempotent: false }, {
537
554
  org: z.string().optional(),
538
555
  app: z.string(),
539
556
  from: z.string().describe("the environment to branch"),
@@ -568,7 +585,7 @@ async function runMcpServer(options = {}) {
568
585
  grants
569
586
  })).branch;
570
587
  });
571
- tool("delete_branch", "Tear down a branch config and every value it overrode. The parent environment is untouched.", {
588
+ tool("delete_branch", "Tear down a branch config and every value it overrode. The parent environment is untouched.", destructive, {
572
589
  org: z.string().optional(),
573
590
  app: z.string(),
574
591
  branch: z.string()
@@ -582,7 +599,7 @@ async function runMcpServer(options = {}) {
582
599
  await ctx.client.deleteBranch(appRef.orgId, target.id);
583
600
  return { deleted: target.slug };
584
601
  });
585
- tool("create_group_env", "Create a group environment. Generates the data key locally and grants it to the caller.", {
602
+ tool("create_group_env", "Create a group environment. Generates the data key locally and grants it to the caller.", { idempotent: false }, {
586
603
  org: z.string().optional(),
587
604
  group: z.string(),
588
605
  name: z.string(),
@@ -600,7 +617,7 @@ async function runMcpServer(options = {}) {
600
617
  wrappedDek
601
618
  })).environment;
602
619
  });
603
- tool("compose_group", "Compose a group into an application environment (higher position wins on name clashes).", {
620
+ tool("compose_group", "Compose a group into an application environment (higher position wins on name clashes).", { idempotent: true }, {
604
621
  org: z.string().optional(),
605
622
  app: z.string(),
606
623
  env: z.string(),
@@ -622,7 +639,7 @@ async function runMcpServer(options = {}) {
622
639
  position
623
640
  })).group;
624
641
  });
625
- tool("uncompose_group", "Remove a composed group from an application environment.", {
642
+ tool("uncompose_group", "Remove a composed group from an application environment.", { idempotent: true }, {
626
643
  org: z.string().optional(),
627
644
  app: z.string(),
628
645
  env: z.string(),
@@ -641,7 +658,7 @@ async function runMcpServer(options = {}) {
641
658
  await ctx.client.unlinkEnvGroup(target.orgId, target.envId, g.id);
642
659
  return { ok: true };
643
660
  });
644
- 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.", {
661
+ 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 }, {
645
662
  ...targetShape,
646
663
  name: z.string(),
647
664
  value: z.string()
@@ -655,7 +672,7 @@ async function runMcpServer(options = {}) {
655
672
  name: o.name
656
673
  };
657
674
  });
658
- 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). A revealed current value has its ${OTHER_SECRET} references expanded against this environment's own secrets; pass raw:true for the stored text instead. Pass `version` to read an earlier version instead of the current one (always as stored, never expanded).", {
675
+ 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). A revealed current value has its ${OTHER_SECRET} references expanded against this environment's own secrets; pass raw:true for the stored text instead. Pass `version` to read an earlier version instead of the current one (always as stored, never expanded).", ro, {
659
676
  ...targetShape,
660
677
  name: z.string(),
661
678
  reveal: z.boolean().optional(),
@@ -692,7 +709,7 @@ async function runMcpServer(options = {}) {
692
709
  revealed: true
693
710
  };
694
711
  });
695
- 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.", {
712
+ 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.", ro, {
696
713
  ...targetShape,
697
714
  name: z.string(),
698
715
  limit: z.number().int().min(1).max(200).optional().describe("default 20")
@@ -710,7 +727,7 @@ async function runMcpServer(options = {}) {
710
727
  }))
711
728
  };
712
729
  });
713
- 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.", {
730
+ 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 }, {
714
731
  ...targetShape,
715
732
  name: z.string(),
716
733
  version: z.number().int().positive()
@@ -725,7 +742,7 @@ async function runMcpServer(options = {}) {
725
742
  version: secret.version
726
743
  };
727
744
  });
728
- tool("delete_secret", "Delete a secret from an environment.", {
745
+ tool("delete_secret", "Delete a secret from an environment.", destructive, {
729
746
  ...targetShape,
730
747
  name: z.string()
731
748
  }, async (o) => {
@@ -738,6 +755,9 @@ async function runMcpServer(options = {}) {
738
755
  };
739
756
  });
740
757
  tool("run_command", "Run a command with the resolved secrets injected as environment variables, and return its exit code + captured output. Secret VALUES are never returned — this is the preferred way to use secrets. process env > .env > app env > groups.", {
758
+ destructive: true,
759
+ idempotent: false
760
+ }, {
741
761
  command: z.string().describe("executable to run"),
742
762
  args: z.array(z.string()).optional(),
743
763
  org: z.string().optional(),
@@ -760,7 +780,7 @@ async function runMcpServer(options = {}) {
760
780
  injectedVarCount: Object.keys(values).length
761
781
  };
762
782
  });
763
- tool("export_env", "Write the resolved secrets to a dotenv file on disk and return the variable names written (never the values). Use to materialize a .env for local tooling.", {
783
+ tool("export_env", "Write the resolved secrets to a dotenv file on disk and return the variable names written (never the values). Use to materialize a .env for local tooling.", destructive, {
764
784
  file: z.string().describe("path to write, e.g. .env"),
765
785
  org: z.string().optional(),
766
786
  app: z.string().optional(),
@@ -779,7 +799,7 @@ async function runMcpServer(options = {}) {
779
799
  names: Object.keys(values).sort()
780
800
  };
781
801
  });
782
- tool("create_token", "Mint a service token, printed once. Runtime tokens bind to one app environment (auto-granted its keys, so a command/agent can decrypt it). Pass admin:true for an org-scoped provisioning token (create apps/groups/envs, grant, mint tokens) — admin tokens need no env binding.", {
802
+ tool("create_token", "Mint a service token, printed once. Runtime tokens bind to one app environment (auto-granted its keys, so a command/agent can decrypt it). Pass admin:true for an org-scoped provisioning token (create apps/groups/envs, grant, mint tokens) — admin tokens need no env binding.", { idempotent: false }, {
783
803
  name: z.string().describe("display name, e.g. ci-deploy or agent-session"),
784
804
  org: z.string().optional(),
785
805
  app: z.string().optional().describe("bind to this app (runtime tokens)"),
@@ -830,7 +850,7 @@ async function runMcpServer(options = {}) {
830
850
  note: "save this now — the secret token string is not stored and cannot be retrieved"
831
851
  };
832
852
  });
833
- tool("revoke_token", "Revoke a service token by id.", {
853
+ tool("revoke_token", "Revoke a service token by id.", destructive, {
834
854
  org: z.string().optional(),
835
855
  tokenId: z.string()
836
856
  }, async ({ org, tokenId }) => {
@@ -842,7 +862,7 @@ async function runMcpServer(options = {}) {
842
862
  tokenId
843
863
  };
844
864
  });
845
- tool("grant_env", "Grant a member (by email) or service token (by id) access to an environment's data key. Re-wraps the DEK to the grantee — the caller must already hold the key.", {
865
+ tool("grant_env", "Grant a member (by email) or service token (by id) access to an environment's data key. Re-wraps the DEK to the grantee — the caller must already hold the key.", { idempotent: true }, {
846
866
  ...targetShape,
847
867
  user: z.string().optional().describe("org member email"),
848
868
  token: z.string().optional().describe("service token id (skt_…)")
@@ -887,12 +907,12 @@ async function runMcpServer(options = {}) {
887
907
  principalId
888
908
  };
889
909
  });
890
- tool("list_pg_targets", "List registered Postgres provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
910
+ tool("list_pg_targets", "List registered Postgres provisioning targets for temporary credentials.", ro, { org: z.string().optional() }, async ({ org }) => {
891
911
  const ctx = getCtx();
892
912
  const orgRef = await resolveOrg(ctx, org);
893
913
  return (await ctx.client.listLeaseTargets(orgRef.id)).targets;
894
914
  });
895
- tool("create_pg_lease", "Mint a short-lived Postgres credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its SCRAM verifier is sent to the API — the plaintext never reaches seekrit or Postgres at rest. The role auto-expires; revoke early with revoke_pg_lease.", {
915
+ tool("create_pg_lease", "Mint a short-lived Postgres credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its SCRAM verifier is sent to the API — the plaintext never reaches seekrit or Postgres at rest. The role auto-expires; revoke early with revoke_pg_lease.", { idempotent: false }, {
896
916
  org: z.string().optional(),
897
917
  target: z.string().describe("target id or name"),
898
918
  role: z.string().optional().describe("role name to create (default: random tmp_ name)"),
@@ -921,12 +941,12 @@ async function runMcpServer(options = {}) {
921
941
  note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
922
942
  };
923
943
  });
924
- tool("list_pg_leases", "List Postgres leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
944
+ tool("list_pg_leases", "List Postgres leases (the ledger — never secret material).", ro, { org: z.string().optional() }, async ({ org }) => {
925
945
  const ctx = getCtx();
926
946
  const orgRef = await resolveOrg(ctx, org);
927
947
  return (await ctx.client.listLeases(orgRef.id)).leases;
928
948
  });
929
- tool("revoke_pg_lease", "Revoke a Postgres lease now (drops the role immediately).", {
949
+ tool("revoke_pg_lease", "Revoke a Postgres lease now (drops the role immediately).", destructive, {
930
950
  org: z.string().optional(),
931
951
  leaseId: z.string()
932
952
  }, async ({ org, leaseId }) => {
@@ -938,12 +958,12 @@ async function runMcpServer(options = {}) {
938
958
  leaseId
939
959
  };
940
960
  });
941
- tool("list_mysql_targets", "List registered MySQL/MariaDB provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
961
+ tool("list_mysql_targets", "List registered MySQL/MariaDB provisioning targets for temporary credentials.", ro, { org: z.string().optional() }, async ({ org }) => {
942
962
  const ctx = getCtx();
943
963
  const orgRef = await resolveOrg(ctx, org);
944
964
  return (await ctx.client.listLeaseTargets(orgRef.id)).targets.filter((t) => t.provider === "mysql");
945
965
  });
946
- tool("create_mysql_lease", "Mint a short-lived MySQL/MariaDB credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its mysql_native_password hash is sent to the API — the plaintext never reaches seekrit or MySQL at rest. The user auto-expires; revoke early with revoke_mysql_lease.", {
966
+ tool("create_mysql_lease", "Mint a short-lived MySQL/MariaDB credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its mysql_native_password hash is sent to the API — the plaintext never reaches seekrit or MySQL at rest. The user auto-expires; revoke early with revoke_mysql_lease.", { idempotent: false }, {
947
967
  org: z.string().optional(),
948
968
  target: z.string().describe("target id or name"),
949
969
  user: z.string().optional().describe("user name to create (default: random tmp_ name)"),
@@ -973,12 +993,12 @@ async function runMcpServer(options = {}) {
973
993
  note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
974
994
  };
975
995
  });
976
- tool("list_mysql_leases", "List MySQL/MariaDB leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
996
+ tool("list_mysql_leases", "List MySQL/MariaDB leases (the ledger — never secret material).", ro, { org: z.string().optional() }, async ({ org }) => {
977
997
  const ctx = getCtx();
978
998
  const orgRef = await resolveOrg(ctx, org);
979
999
  return (await ctx.client.listLeases(orgRef.id)).leases.filter((l) => l.provider === "mysql");
980
1000
  });
981
- tool("revoke_mysql_lease", "Revoke a MySQL/MariaDB lease now (drops the user immediately).", {
1001
+ tool("revoke_mysql_lease", "Revoke a MySQL/MariaDB lease now (drops the user immediately).", destructive, {
982
1002
  org: z.string().optional(),
983
1003
  leaseId: z.string()
984
1004
  }, async ({ org, leaseId }) => {
@@ -990,7 +1010,7 @@ async function runMcpServer(options = {}) {
990
1010
  leaseId
991
1011
  };
992
1012
  });
993
- tool("configure_project", "Link a directory to an org/app by writing seekrit.json (like `seekrit init`). The environment is chosen by the service token at runtime.", {
1013
+ tool("configure_project", "Link a directory to an org/app by writing seekrit.json (like `seekrit init`). The environment is chosen by the service token at runtime.", destructive, {
994
1014
  org: z.string(),
995
1015
  app: z.string(),
996
1016
  dir: z.string().optional()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,9 +27,9 @@
27
27
  "@types/node": "^26.1.0",
28
28
  "tsdown": "^0.22.3",
29
29
  "vitest": "^4.1.9",
30
- "@seekrit/core": "0.0.1",
31
30
  "@seekrit/api-client": "0.0.1",
32
- "@seekrit/crypto": "0.0.1"
31
+ "@seekrit/crypto": "0.0.1",
32
+ "@seekrit/core": "0.0.1"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsdown",