@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/README.md +196 -6
- package/dist/{chunk-F4SPZ774.js → chunk-6IQ5SFLI.js} +298 -6
- package/dist/chunk-6IQ5SFLI.js.map +1 -0
- package/dist/{chunk-3WTTWJYU.js → chunk-IGNU622R.js} +337 -5
- package/dist/chunk-IGNU622R.js.map +1 -0
- package/dist/{dashboard-X3ONQFLV.js → dashboard-32PCZF7D.js} +2 -2
- package/dist/{dashboard-QQWKOOI5.js → dashboard-HVIQO6NT.js} +2 -2
- package/dist/index.js +739 -53
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +580 -9
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3WTTWJYU.js.map +0 -1
- package/dist/chunk-F4SPZ774.js.map +0 -1
- /package/dist/{dashboard-QQWKOOI5.js.map → dashboard-32PCZF7D.js.map} +0 -0
- /package/dist/{dashboard-X3ONQFLV.js.map → dashboard-HVIQO6NT.js.map} +0 -0
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-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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.`);
|