@i4ctime/q-ring 0.3.1 → 0.4.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/mcp.js CHANGED
@@ -4,19 +4,26 @@ import {
4
4
  collapseEnvironment,
5
5
  deleteSecret,
6
6
  detectAnomalies,
7
+ disentangleSecrets,
7
8
  entangleSecrets,
9
+ exportSecrets,
10
+ fireHooks,
8
11
  getEnvelope,
9
12
  getSecret,
10
13
  hasSecret,
14
+ listHooks,
11
15
  listSecrets,
12
16
  logAudit,
13
17
  queryAudit,
18
+ readProjectConfig,
19
+ registerHook,
20
+ removeHook,
14
21
  setSecret,
15
22
  tunnelCreate,
16
23
  tunnelDestroy,
17
24
  tunnelList,
18
25
  tunnelRead
19
- } from "./chunk-3WTTWJYU.js";
26
+ } from "./chunk-6IQ5SFLI.js";
20
27
 
21
28
  // src/mcp.ts
22
29
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -147,7 +154,9 @@ function runHealthScan(config = {}) {
147
154
  `EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
148
155
  );
149
156
  if (cfg.autoRotate) {
150
- const newValue = generateSecret({ format: "api-key" });
157
+ const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
158
+ const prefix = entry.envelope?.meta.rotationPrefix;
159
+ const newValue = generateSecret({ format: fmt, prefix });
151
160
  setSecret(entry.key, newValue, {
152
161
  scope: entry.scope,
153
162
  projectPath: cfg.projectPaths[0],
@@ -161,6 +170,14 @@ function runHealthScan(config = {}) {
161
170
  source: "agent",
162
171
  detail: "auto-rotated by agent (expired)"
163
172
  });
173
+ fireHooks({
174
+ action: "rotate",
175
+ key: entry.key,
176
+ scope: entry.scope,
177
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
178
+ source: "agent"
179
+ }, entry.envelope?.meta.tags).catch(() => {
180
+ });
164
181
  }
165
182
  } else if (decay.isStale) {
166
183
  report.stale++;
@@ -240,6 +257,262 @@ function teleportUnpack(encoded, passphrase) {
240
257
  return JSON.parse(decrypted.toString("utf8"));
241
258
  }
242
259
 
260
+ // src/core/import.ts
261
+ import { readFileSync } from "fs";
262
+ function parseDotenv(content) {
263
+ const result = /* @__PURE__ */ new Map();
264
+ const lines = content.split(/\r?\n/);
265
+ for (let i = 0; i < lines.length; i++) {
266
+ const line = lines[i].trim();
267
+ if (!line || line.startsWith("#")) continue;
268
+ const eqIdx = line.indexOf("=");
269
+ if (eqIdx === -1) continue;
270
+ const key = line.slice(0, eqIdx).trim();
271
+ let value = line.slice(eqIdx + 1).trim();
272
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
273
+ value = value.slice(1, -1);
274
+ }
275
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
276
+ if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
277
+ value = value.split("#")[0].trim();
278
+ }
279
+ if (key) result.set(key, value);
280
+ }
281
+ return result;
282
+ }
283
+ function importDotenv(filePathOrContent, options = {}) {
284
+ let content;
285
+ try {
286
+ content = readFileSync(filePathOrContent, "utf8");
287
+ } catch {
288
+ content = filePathOrContent;
289
+ }
290
+ const pairs = parseDotenv(content);
291
+ const result = {
292
+ imported: [],
293
+ skipped: [],
294
+ total: pairs.size
295
+ };
296
+ for (const [key, value] of pairs) {
297
+ if (options.skipExisting && hasSecret(key, {
298
+ scope: options.scope,
299
+ projectPath: options.projectPath,
300
+ source: options.source ?? "cli"
301
+ })) {
302
+ result.skipped.push(key);
303
+ continue;
304
+ }
305
+ if (options.dryRun) {
306
+ result.imported.push(key);
307
+ continue;
308
+ }
309
+ const setOpts = {
310
+ scope: options.scope ?? "global",
311
+ projectPath: options.projectPath ?? process.cwd(),
312
+ source: options.source ?? "cli"
313
+ };
314
+ setSecret(key, value, setOpts);
315
+ result.imported.push(key);
316
+ }
317
+ return result;
318
+ }
319
+
320
+ // src/core/validate.ts
321
+ import { request as httpsRequest } from "https";
322
+ import { request as httpRequest } from "http";
323
+ function makeRequest(url, headers, timeoutMs = 1e4) {
324
+ return new Promise((resolve, reject) => {
325
+ const parsedUrl = new URL(url);
326
+ const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
327
+ const req = reqFn(
328
+ url,
329
+ { method: "GET", headers, timeout: timeoutMs },
330
+ (res) => {
331
+ let body = "";
332
+ res.on("data", (chunk) => body += chunk);
333
+ res.on(
334
+ "end",
335
+ () => resolve({ statusCode: res.statusCode ?? 0, body })
336
+ );
337
+ }
338
+ );
339
+ req.on("error", reject);
340
+ req.on("timeout", () => {
341
+ req.destroy();
342
+ reject(new Error("Request timed out"));
343
+ });
344
+ req.end();
345
+ });
346
+ }
347
+ var ProviderRegistry = class {
348
+ providers = /* @__PURE__ */ new Map();
349
+ register(provider) {
350
+ this.providers.set(provider.name, provider);
351
+ }
352
+ get(name) {
353
+ return this.providers.get(name);
354
+ }
355
+ detectProvider(value, hints) {
356
+ if (hints?.provider) {
357
+ return this.providers.get(hints.provider);
358
+ }
359
+ for (const provider of this.providers.values()) {
360
+ if (provider.prefixes) {
361
+ for (const pfx of provider.prefixes) {
362
+ if (value.startsWith(pfx)) return provider;
363
+ }
364
+ }
365
+ }
366
+ return void 0;
367
+ }
368
+ listProviders() {
369
+ return [...this.providers.values()];
370
+ }
371
+ };
372
+ var openaiProvider = {
373
+ name: "openai",
374
+ description: "OpenAI API key validation",
375
+ prefixes: ["sk-"],
376
+ async validate(value) {
377
+ const start = Date.now();
378
+ try {
379
+ const { statusCode } = await makeRequest(
380
+ "https://api.openai.com/v1/models?limit=1",
381
+ {
382
+ Authorization: `Bearer ${value}`,
383
+ "User-Agent": "q-ring-validator/1.0"
384
+ }
385
+ );
386
+ const latencyMs = Date.now() - start;
387
+ if (statusCode === 200)
388
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
389
+ if (statusCode === 401)
390
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
391
+ if (statusCode === 429)
392
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
393
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
394
+ } catch (err) {
395
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
396
+ }
397
+ }
398
+ };
399
+ var stripeProvider = {
400
+ name: "stripe",
401
+ description: "Stripe API key validation",
402
+ prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
403
+ async validate(value) {
404
+ const start = Date.now();
405
+ try {
406
+ const { statusCode } = await makeRequest(
407
+ "https://api.stripe.com/v1/balance",
408
+ {
409
+ Authorization: `Bearer ${value}`,
410
+ "User-Agent": "q-ring-validator/1.0"
411
+ }
412
+ );
413
+ const latencyMs = Date.now() - start;
414
+ if (statusCode === 200)
415
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
416
+ if (statusCode === 401)
417
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
418
+ if (statusCode === 429)
419
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
420
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
421
+ } catch (err) {
422
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
423
+ }
424
+ }
425
+ };
426
+ var githubProvider = {
427
+ name: "github",
428
+ description: "GitHub token validation",
429
+ prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
430
+ async validate(value) {
431
+ const start = Date.now();
432
+ try {
433
+ const { statusCode } = await makeRequest(
434
+ "https://api.github.com/user",
435
+ {
436
+ Authorization: `token ${value}`,
437
+ "User-Agent": "q-ring-validator/1.0",
438
+ Accept: "application/vnd.github+json"
439
+ }
440
+ );
441
+ const latencyMs = Date.now() - start;
442
+ if (statusCode === 200)
443
+ return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
444
+ if (statusCode === 401)
445
+ return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
446
+ if (statusCode === 403)
447
+ return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
448
+ if (statusCode === 429)
449
+ return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
450
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
451
+ } catch (err) {
452
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
453
+ }
454
+ }
455
+ };
456
+ var awsProvider = {
457
+ name: "aws",
458
+ description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
459
+ prefixes: ["AKIA", "ASIA"],
460
+ async validate(value) {
461
+ const start = Date.now();
462
+ const latencyMs = Date.now() - start;
463
+ if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
464
+ return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
465
+ }
466
+ return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
467
+ }
468
+ };
469
+ var httpProvider = {
470
+ name: "http",
471
+ description: "Generic HTTP endpoint validation",
472
+ async validate(value, url) {
473
+ const start = Date.now();
474
+ if (!url) {
475
+ return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
476
+ }
477
+ try {
478
+ const { statusCode } = await makeRequest(url, {
479
+ Authorization: `Bearer ${value}`,
480
+ "User-Agent": "q-ring-validator/1.0"
481
+ });
482
+ const latencyMs = Date.now() - start;
483
+ if (statusCode >= 200 && statusCode < 300)
484
+ return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
485
+ if (statusCode === 401 || statusCode === 403)
486
+ return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
487
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
488
+ } catch (err) {
489
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
490
+ }
491
+ }
492
+ };
493
+ var registry = new ProviderRegistry();
494
+ registry.register(openaiProvider);
495
+ registry.register(stripeProvider);
496
+ registry.register(githubProvider);
497
+ registry.register(awsProvider);
498
+ registry.register(httpProvider);
499
+ async function validateSecret(value, opts2) {
500
+ const provider = opts2?.provider ? registry.get(opts2.provider) : registry.detectProvider(value);
501
+ if (!provider) {
502
+ return {
503
+ valid: false,
504
+ status: "unknown",
505
+ message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
506
+ latencyMs: 0,
507
+ provider: "none"
508
+ };
509
+ }
510
+ if (provider.name === "http" && opts2?.validationUrl) {
511
+ return provider.validate(value, opts2.validationUrl);
512
+ }
513
+ return provider.validate(value);
514
+ }
515
+
243
516
  // src/mcp/server.ts
244
517
  function text(t, isError = false) {
245
518
  return {
@@ -280,13 +553,37 @@ function createMcpServer() {
280
553
  );
281
554
  server2.tool(
282
555
  "list_secrets",
283
- "List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed.",
556
+ "List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed. Supports filtering by tag, expiry state, and key pattern.",
284
557
  {
285
558
  scope: scopeSchema,
286
- projectPath: projectPathSchema
559
+ projectPath: projectPathSchema,
560
+ tag: z.string().optional().describe("Filter by tag"),
561
+ expired: z.boolean().optional().describe("Show only expired secrets"),
562
+ stale: z.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
563
+ filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')")
287
564
  },
288
565
  async (params) => {
289
- const entries = listSecrets(opts(params));
566
+ let entries = listSecrets(opts(params));
567
+ if (params.tag) {
568
+ entries = entries.filter(
569
+ (e) => e.envelope?.meta.tags?.includes(params.tag)
570
+ );
571
+ }
572
+ if (params.expired) {
573
+ entries = entries.filter((e) => e.decay?.isExpired);
574
+ }
575
+ if (params.stale) {
576
+ entries = entries.filter(
577
+ (e) => e.decay?.isStale && !e.decay?.isExpired
578
+ );
579
+ }
580
+ if (params.filter) {
581
+ const regex = new RegExp(
582
+ "^" + params.filter.replace(/\*/g, ".*") + "$",
583
+ "i"
584
+ );
585
+ entries = entries.filter((e) => regex.test(e.key));
586
+ }
290
587
  if (entries.length === 0) return text("No secrets found");
291
588
  const lines = entries.map((e) => {
292
589
  const parts = [`[${e.scope}] ${e.key}`];
@@ -323,7 +620,9 @@ function createMcpServer() {
323
620
  env: z.string().optional().describe("If provided, sets the value for this specific environment (superposition)"),
324
621
  ttlSeconds: z.number().optional().describe("Time-to-live in seconds (quantum decay)"),
325
622
  description: z.string().optional().describe("Human-readable description"),
326
- tags: z.array(z.string()).optional().describe("Tags for organization")
623
+ tags: z.array(z.string()).optional().describe("Tags for organization"),
624
+ rotationFormat: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().describe("Format for auto-rotation when this secret expires"),
625
+ rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')")
327
626
  },
328
627
  async (params) => {
329
628
  const o = opts(params);
@@ -340,7 +639,9 @@ function createMcpServer() {
340
639
  defaultEnv: existing?.envelope?.defaultEnv ?? params.env,
341
640
  ttlSeconds: params.ttlSeconds,
342
641
  description: params.description,
343
- tags: params.tags
642
+ tags: params.tags,
643
+ rotationFormat: params.rotationFormat,
644
+ rotationPrefix: params.rotationPrefix
344
645
  });
345
646
  return text(`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`);
346
647
  }
@@ -348,7 +649,9 @@ function createMcpServer() {
348
649
  ...o,
349
650
  ttlSeconds: params.ttlSeconds,
350
651
  description: params.description,
351
- tags: params.tags
652
+ tags: params.tags,
653
+ rotationFormat: params.rotationFormat,
654
+ rotationPrefix: params.rotationPrefix
352
655
  });
353
656
  return text(`[${params.scope ?? "global"}] ${params.key} saved`);
354
657
  }
@@ -381,6 +684,152 @@ function createMcpServer() {
381
684
  return text(hasSecret(params.key, opts(params)) ? "true" : "false");
382
685
  }
383
686
  );
687
+ server2.tool(
688
+ "export_secrets",
689
+ "Export secrets as .env or JSON format. Collapses superposition. Supports filtering by specific keys or tags.",
690
+ {
691
+ format: z.enum(["env", "json"]).optional().default("env").describe("Output format"),
692
+ keys: z.array(z.string()).optional().describe("Only export these specific key names"),
693
+ tags: z.array(z.string()).optional().describe("Only export secrets with any of these tags"),
694
+ scope: scopeSchema,
695
+ projectPath: projectPathSchema,
696
+ env: envSchema
697
+ },
698
+ async (params) => {
699
+ const output = exportSecrets({
700
+ ...opts(params),
701
+ format: params.format,
702
+ keys: params.keys,
703
+ tags: params.tags
704
+ });
705
+ if (!output.trim()) return text("No secrets matched the filters", true);
706
+ return text(output);
707
+ }
708
+ );
709
+ server2.tool(
710
+ "import_dotenv",
711
+ "Import secrets from .env file content. Parses standard dotenv syntax (comments, quotes, multiline escapes) and stores each key/value pair in q-ring.",
712
+ {
713
+ content: z.string().describe("The .env file content to parse and import"),
714
+ scope: scopeSchema.default("global"),
715
+ projectPath: projectPathSchema,
716
+ skipExisting: z.boolean().optional().default(false).describe("Skip keys that already exist in q-ring"),
717
+ dryRun: z.boolean().optional().default(false).describe("Preview what would be imported without saving")
718
+ },
719
+ async (params) => {
720
+ const result = importDotenv(params.content, {
721
+ scope: params.scope,
722
+ projectPath: params.projectPath ?? process.cwd(),
723
+ source: "mcp",
724
+ skipExisting: params.skipExisting,
725
+ dryRun: params.dryRun
726
+ });
727
+ const lines = [
728
+ params.dryRun ? "Dry run \u2014 no changes made" : `Imported ${result.imported.length} secret(s)`
729
+ ];
730
+ if (result.imported.length > 0) {
731
+ lines.push(`Keys: ${result.imported.join(", ")}`);
732
+ }
733
+ if (result.skipped.length > 0) {
734
+ lines.push(`Skipped (existing): ${result.skipped.join(", ")}`);
735
+ }
736
+ return text(lines.join("\n"));
737
+ }
738
+ );
739
+ server2.tool(
740
+ "check_project",
741
+ "Validate project secrets against the .q-ring.json manifest. Returns which required secrets are present, missing, expired, or stale. Use this to verify project readiness.",
742
+ {
743
+ projectPath: projectPathSchema
744
+ },
745
+ async (params) => {
746
+ const projectPath = params.projectPath ?? process.cwd();
747
+ const config = readProjectConfig(projectPath);
748
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
749
+ return text("No secrets manifest found in .q-ring.json", true);
750
+ }
751
+ const results = [];
752
+ let presentCount = 0;
753
+ let missingCount = 0;
754
+ let expiredCount = 0;
755
+ let staleCount = 0;
756
+ for (const [key, manifest] of Object.entries(config.secrets)) {
757
+ const result = getEnvelope(key, { projectPath, source: "mcp" });
758
+ if (!result) {
759
+ const status = manifest.required !== false ? "missing" : "optional_missing";
760
+ if (manifest.required !== false) missingCount++;
761
+ results.push({ key, status, required: manifest.required !== false, description: manifest.description });
762
+ continue;
763
+ }
764
+ const decay = checkDecay(result.envelope);
765
+ if (decay.isExpired) {
766
+ expiredCount++;
767
+ results.push({ key, status: "expired", timeRemaining: decay.timeRemaining, description: manifest.description });
768
+ } else if (decay.isStale) {
769
+ staleCount++;
770
+ results.push({ key, status: "stale", lifetimePercent: decay.lifetimePercent, timeRemaining: decay.timeRemaining, description: manifest.description });
771
+ } else {
772
+ presentCount++;
773
+ results.push({ key, status: "ok", description: manifest.description });
774
+ }
775
+ }
776
+ const summary = {
777
+ total: Object.keys(config.secrets).length,
778
+ present: presentCount,
779
+ missing: missingCount,
780
+ expired: expiredCount,
781
+ stale: staleCount,
782
+ ready: missingCount === 0 && expiredCount === 0,
783
+ secrets: results
784
+ };
785
+ return text(JSON.stringify(summary, null, 2));
786
+ }
787
+ );
788
+ server2.tool(
789
+ "env_generate",
790
+ "Generate .env file content from the project manifest (.q-ring.json). Resolves each declared secret from q-ring, collapses superposition, and returns .env formatted output. Warns about missing or expired secrets.",
791
+ {
792
+ projectPath: projectPathSchema,
793
+ env: envSchema
794
+ },
795
+ async (params) => {
796
+ const projectPath = params.projectPath ?? process.cwd();
797
+ const config = readProjectConfig(projectPath);
798
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
799
+ return text("No secrets manifest found in .q-ring.json", true);
800
+ }
801
+ const lines = [];
802
+ const warnings = [];
803
+ for (const [key, manifest] of Object.entries(config.secrets)) {
804
+ const value = getSecret(key, {
805
+ projectPath,
806
+ env: params.env,
807
+ source: "mcp"
808
+ });
809
+ if (value === null) {
810
+ if (manifest.required !== false) {
811
+ warnings.push(`MISSING (required): ${key}`);
812
+ }
813
+ lines.push(`# ${key}=`);
814
+ continue;
815
+ }
816
+ const result2 = getEnvelope(key, { projectPath, source: "mcp" });
817
+ if (result2) {
818
+ const decay = checkDecay(result2.envelope);
819
+ if (decay.isExpired) warnings.push(`EXPIRED: ${key}`);
820
+ else if (decay.isStale) warnings.push(`STALE: ${key}`);
821
+ }
822
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
823
+ lines.push(`${key}="${escaped}"`);
824
+ }
825
+ const output = lines.join("\n");
826
+ const result = warnings.length > 0 ? `${output}
827
+
828
+ # Warnings:
829
+ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
830
+ return text(result);
831
+ }
832
+ );
384
833
  server2.tool(
385
834
  "inspect_secret",
386
835
  "Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.",
@@ -500,6 +949,35 @@ function createMcpServer() {
500
949
  return text(`Entangled: ${params.sourceKey} <-> ${params.targetKey}`);
501
950
  }
502
951
  );
952
+ server2.tool(
953
+ "disentangle_secrets",
954
+ "Remove a quantum entanglement between two secrets. They will no longer synchronize on rotation.",
955
+ {
956
+ sourceKey: z.string().describe("Source secret key"),
957
+ targetKey: z.string().describe("Target secret key"),
958
+ sourceScope: scopeSchema.default("global"),
959
+ targetScope: scopeSchema.default("global"),
960
+ sourceProjectPath: z.string().optional(),
961
+ targetProjectPath: z.string().optional()
962
+ },
963
+ async (params) => {
964
+ disentangleSecrets(
965
+ params.sourceKey,
966
+ {
967
+ scope: params.sourceScope,
968
+ projectPath: params.sourceProjectPath ?? process.cwd(),
969
+ source: "mcp"
970
+ },
971
+ params.targetKey,
972
+ {
973
+ scope: params.targetScope,
974
+ projectPath: params.targetProjectPath ?? process.cwd(),
975
+ source: "mcp"
976
+ }
977
+ );
978
+ return text(`Disentangled: ${params.sourceKey} </> ${params.targetKey}`);
979
+ }
980
+ );
503
981
  server2.tool(
504
982
  "tunnel_create",
505
983
  "Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.",
@@ -708,6 +1186,99 @@ ${preview}`);
708
1186
  return text(summary.join("\n"));
709
1187
  }
710
1188
  );
1189
+ server2.tool(
1190
+ "validate_secret",
1191
+ "Test if a secret is actually valid with its target service (e.g., OpenAI, Stripe, GitHub). Uses provider auto-detection based on key prefixes, or accepts an explicit provider name. Never logs the secret value.",
1192
+ {
1193
+ key: z.string().describe("The secret key name"),
1194
+ provider: z.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
1195
+ scope: scopeSchema,
1196
+ projectPath: projectPathSchema
1197
+ },
1198
+ async (params) => {
1199
+ const value = getSecret(params.key, opts(params));
1200
+ if (value === null) return text(`Secret "${params.key}" not found`, true);
1201
+ const envelope = getEnvelope(params.key, opts(params));
1202
+ const provHint = params.provider ?? envelope?.envelope.meta.provider;
1203
+ const result = await validateSecret(value, { provider: provHint });
1204
+ return text(JSON.stringify(result, null, 2));
1205
+ }
1206
+ );
1207
+ server2.tool(
1208
+ "list_providers",
1209
+ "List all available validation providers for secret liveness testing.",
1210
+ {},
1211
+ async () => {
1212
+ const providers = registry.listProviders().map((p) => ({
1213
+ name: p.name,
1214
+ description: p.description,
1215
+ prefixes: p.prefixes ?? []
1216
+ }));
1217
+ return text(JSON.stringify(providers, null, 2));
1218
+ }
1219
+ );
1220
+ server2.tool(
1221
+ "register_hook",
1222
+ "Register a webhook/callback that fires when a secret is updated, deleted, or rotated. Supports shell commands, HTTP webhooks, and process signals.",
1223
+ {
1224
+ type: z.enum(["shell", "http", "signal"]).describe("Hook type"),
1225
+ key: z.string().optional().describe("Trigger on exact key match"),
1226
+ keyPattern: z.string().optional().describe("Trigger on key glob pattern (e.g. DB_*)"),
1227
+ tag: z.string().optional().describe("Trigger on secrets with this tag"),
1228
+ scope: z.enum(["global", "project"]).optional().describe("Trigger only for this scope"),
1229
+ actions: z.array(z.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which actions trigger this hook"),
1230
+ command: z.string().optional().describe("Shell command to execute (for shell type)"),
1231
+ url: z.string().optional().describe("URL to POST to (for http type)"),
1232
+ signalTarget: z.string().optional().describe("Process name or PID (for signal type)"),
1233
+ signalName: z.string().optional().default("SIGHUP").describe("Signal to send (for signal type)"),
1234
+ description: z.string().optional().describe("Human-readable description")
1235
+ },
1236
+ async (params) => {
1237
+ if (!params.key && !params.keyPattern && !params.tag) {
1238
+ return text("At least one match criterion required: key, keyPattern, or tag", true);
1239
+ }
1240
+ const entry = registerHook({
1241
+ type: params.type,
1242
+ match: {
1243
+ key: params.key,
1244
+ keyPattern: params.keyPattern,
1245
+ tag: params.tag,
1246
+ scope: params.scope,
1247
+ action: params.actions
1248
+ },
1249
+ command: params.command,
1250
+ url: params.url,
1251
+ signal: params.signalTarget ? { target: params.signalTarget, signal: params.signalName } : void 0,
1252
+ description: params.description,
1253
+ enabled: true
1254
+ });
1255
+ return text(JSON.stringify(entry, null, 2));
1256
+ }
1257
+ );
1258
+ server2.tool(
1259
+ "list_hooks",
1260
+ "List all registered secret change hooks with their match criteria, type, and status.",
1261
+ {},
1262
+ async () => {
1263
+ const hooks = listHooks();
1264
+ if (hooks.length === 0) return text("No hooks registered");
1265
+ return text(JSON.stringify(hooks, null, 2));
1266
+ }
1267
+ );
1268
+ server2.tool(
1269
+ "remove_hook",
1270
+ "Remove a registered hook by ID.",
1271
+ {
1272
+ id: z.string().describe("Hook ID to remove")
1273
+ },
1274
+ async (params) => {
1275
+ const removed = removeHook(params.id);
1276
+ return text(
1277
+ removed ? `Removed hook ${params.id}` : `Hook "${params.id}" not found`,
1278
+ !removed
1279
+ );
1280
+ }
1281
+ );
711
1282
  let dashboardInstance = null;
712
1283
  server2.tool(
713
1284
  "status_dashboard",
@@ -719,7 +1290,7 @@ ${preview}`);
719
1290
  if (dashboardInstance) {
720
1291
  return text(`Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`);
721
1292
  }
722
- const { startDashboardServer } = await import("./dashboard-QQWKOOI5.js");
1293
+ const { startDashboardServer } = await import("./dashboard-32PCZF7D.js");
723
1294
  dashboardInstance = startDashboardServer({ port: params.port });
724
1295
  return text(`Dashboard started at http://127.0.0.1:${dashboardInstance.port}
725
1296
  Open this URL in a browser to see live quantum status.`);