@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/index.js
CHANGED
|
@@ -4,19 +4,28 @@ import {
|
|
|
4
4
|
collapseEnvironment,
|
|
5
5
|
deleteSecret,
|
|
6
6
|
detectAnomalies,
|
|
7
|
+
disableHook,
|
|
8
|
+
disentangleSecrets,
|
|
9
|
+
enableHook,
|
|
7
10
|
entangleSecrets,
|
|
8
11
|
exportSecrets,
|
|
12
|
+
fireHooks,
|
|
9
13
|
getEnvelope,
|
|
10
14
|
getSecret,
|
|
15
|
+
hasSecret,
|
|
16
|
+
listHooks,
|
|
11
17
|
listSecrets,
|
|
12
18
|
logAudit,
|
|
13
19
|
queryAudit,
|
|
20
|
+
readProjectConfig,
|
|
21
|
+
registerHook,
|
|
22
|
+
removeHook,
|
|
14
23
|
setSecret,
|
|
15
24
|
tunnelCreate,
|
|
16
25
|
tunnelDestroy,
|
|
17
26
|
tunnelList,
|
|
18
27
|
tunnelRead
|
|
19
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IGNU622R.js";
|
|
20
29
|
|
|
21
30
|
// src/cli/commands.ts
|
|
22
31
|
import { Command } from "commander";
|
|
@@ -207,7 +216,9 @@ function runHealthScan(config = {}) {
|
|
|
207
216
|
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
208
217
|
);
|
|
209
218
|
if (cfg.autoRotate) {
|
|
210
|
-
const
|
|
219
|
+
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
220
|
+
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
221
|
+
const newValue = generateSecret({ format: fmt, prefix });
|
|
211
222
|
setSecret(entry.key, newValue, {
|
|
212
223
|
scope: entry.scope,
|
|
213
224
|
projectPath: cfg.projectPaths[0],
|
|
@@ -221,6 +232,14 @@ function runHealthScan(config = {}) {
|
|
|
221
232
|
source: "agent",
|
|
222
233
|
detail: "auto-rotated by agent (expired)"
|
|
223
234
|
});
|
|
235
|
+
fireHooks({
|
|
236
|
+
action: "rotate",
|
|
237
|
+
key: entry.key,
|
|
238
|
+
scope: entry.scope,
|
|
239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
source: "agent"
|
|
241
|
+
}, entry.envelope?.meta.tags).catch(() => {
|
|
242
|
+
});
|
|
224
243
|
}
|
|
225
244
|
} else if (decay.isStale) {
|
|
226
245
|
report.stale++;
|
|
@@ -353,6 +372,265 @@ function teleportUnpack(encoded, passphrase) {
|
|
|
353
372
|
return JSON.parse(decrypted.toString("utf8"));
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
// src/core/import.ts
|
|
376
|
+
import { readFileSync } from "fs";
|
|
377
|
+
function parseDotenv(content) {
|
|
378
|
+
const result = /* @__PURE__ */ new Map();
|
|
379
|
+
const lines = content.split(/\r?\n/);
|
|
380
|
+
for (let i = 0; i < lines.length; i++) {
|
|
381
|
+
const line = lines[i].trim();
|
|
382
|
+
if (!line || line.startsWith("#")) continue;
|
|
383
|
+
const eqIdx = line.indexOf("=");
|
|
384
|
+
if (eqIdx === -1) continue;
|
|
385
|
+
const key = line.slice(0, eqIdx).trim();
|
|
386
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
387
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
388
|
+
value = value.slice(1, -1);
|
|
389
|
+
}
|
|
390
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
|
|
391
|
+
if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
|
|
392
|
+
value = value.split("#")[0].trim();
|
|
393
|
+
}
|
|
394
|
+
if (key) result.set(key, value);
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function importDotenv(filePathOrContent, options = {}) {
|
|
399
|
+
let content;
|
|
400
|
+
try {
|
|
401
|
+
content = readFileSync(filePathOrContent, "utf8");
|
|
402
|
+
} catch {
|
|
403
|
+
content = filePathOrContent;
|
|
404
|
+
}
|
|
405
|
+
const pairs = parseDotenv(content);
|
|
406
|
+
const result = {
|
|
407
|
+
imported: [],
|
|
408
|
+
skipped: [],
|
|
409
|
+
total: pairs.size
|
|
410
|
+
};
|
|
411
|
+
for (const [key, value] of pairs) {
|
|
412
|
+
if (options.skipExisting && hasSecret(key, {
|
|
413
|
+
scope: options.scope,
|
|
414
|
+
projectPath: options.projectPath,
|
|
415
|
+
source: options.source ?? "cli"
|
|
416
|
+
})) {
|
|
417
|
+
result.skipped.push(key);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (options.dryRun) {
|
|
421
|
+
result.imported.push(key);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const setOpts = {
|
|
425
|
+
scope: options.scope ?? "global",
|
|
426
|
+
projectPath: options.projectPath ?? process.cwd(),
|
|
427
|
+
source: options.source ?? "cli"
|
|
428
|
+
};
|
|
429
|
+
setSecret(key, value, setOpts);
|
|
430
|
+
result.imported.push(key);
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/core/validate.ts
|
|
436
|
+
import { request as httpsRequest } from "https";
|
|
437
|
+
import { request as httpRequest } from "http";
|
|
438
|
+
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
const parsedUrl = new URL(url);
|
|
441
|
+
const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
442
|
+
const req = reqFn(
|
|
443
|
+
url,
|
|
444
|
+
{ method: "GET", headers, timeout: timeoutMs },
|
|
445
|
+
(res) => {
|
|
446
|
+
let body = "";
|
|
447
|
+
res.on("data", (chunk) => body += chunk);
|
|
448
|
+
res.on(
|
|
449
|
+
"end",
|
|
450
|
+
() => resolve({ statusCode: res.statusCode ?? 0, body })
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
req.on("error", reject);
|
|
455
|
+
req.on("timeout", () => {
|
|
456
|
+
req.destroy();
|
|
457
|
+
reject(new Error("Request timed out"));
|
|
458
|
+
});
|
|
459
|
+
req.end();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var ProviderRegistry = class {
|
|
463
|
+
providers = /* @__PURE__ */ new Map();
|
|
464
|
+
register(provider) {
|
|
465
|
+
this.providers.set(provider.name, provider);
|
|
466
|
+
}
|
|
467
|
+
get(name) {
|
|
468
|
+
return this.providers.get(name);
|
|
469
|
+
}
|
|
470
|
+
detectProvider(value, hints) {
|
|
471
|
+
if (hints?.provider) {
|
|
472
|
+
return this.providers.get(hints.provider);
|
|
473
|
+
}
|
|
474
|
+
for (const provider of this.providers.values()) {
|
|
475
|
+
if (provider.prefixes) {
|
|
476
|
+
for (const pfx of provider.prefixes) {
|
|
477
|
+
if (value.startsWith(pfx)) return provider;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
listProviders() {
|
|
484
|
+
return [...this.providers.values()];
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var openaiProvider = {
|
|
488
|
+
name: "openai",
|
|
489
|
+
description: "OpenAI API key validation",
|
|
490
|
+
prefixes: ["sk-"],
|
|
491
|
+
async validate(value) {
|
|
492
|
+
const start = Date.now();
|
|
493
|
+
try {
|
|
494
|
+
const { statusCode } = await makeRequest(
|
|
495
|
+
"https://api.openai.com/v1/models?limit=1",
|
|
496
|
+
{
|
|
497
|
+
Authorization: `Bearer ${value}`,
|
|
498
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
const latencyMs = Date.now() - start;
|
|
502
|
+
if (statusCode === 200)
|
|
503
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
|
|
504
|
+
if (statusCode === 401)
|
|
505
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
|
|
506
|
+
if (statusCode === 429)
|
|
507
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
|
|
508
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var stripeProvider = {
|
|
515
|
+
name: "stripe",
|
|
516
|
+
description: "Stripe API key validation",
|
|
517
|
+
prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
|
|
518
|
+
async validate(value) {
|
|
519
|
+
const start = Date.now();
|
|
520
|
+
try {
|
|
521
|
+
const { statusCode } = await makeRequest(
|
|
522
|
+
"https://api.stripe.com/v1/balance",
|
|
523
|
+
{
|
|
524
|
+
Authorization: `Bearer ${value}`,
|
|
525
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
const latencyMs = Date.now() - start;
|
|
529
|
+
if (statusCode === 200)
|
|
530
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
|
|
531
|
+
if (statusCode === 401)
|
|
532
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
|
|
533
|
+
if (statusCode === 429)
|
|
534
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
|
|
535
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
var githubProvider = {
|
|
542
|
+
name: "github",
|
|
543
|
+
description: "GitHub token validation",
|
|
544
|
+
prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
|
|
545
|
+
async validate(value) {
|
|
546
|
+
const start = Date.now();
|
|
547
|
+
try {
|
|
548
|
+
const { statusCode } = await makeRequest(
|
|
549
|
+
"https://api.github.com/user",
|
|
550
|
+
{
|
|
551
|
+
Authorization: `token ${value}`,
|
|
552
|
+
"User-Agent": "q-ring-validator/1.0",
|
|
553
|
+
Accept: "application/vnd.github+json"
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
const latencyMs = Date.now() - start;
|
|
557
|
+
if (statusCode === 200)
|
|
558
|
+
return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
|
|
559
|
+
if (statusCode === 401)
|
|
560
|
+
return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
|
|
561
|
+
if (statusCode === 403)
|
|
562
|
+
return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
|
|
563
|
+
if (statusCode === 429)
|
|
564
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
|
|
565
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
var awsProvider = {
|
|
572
|
+
name: "aws",
|
|
573
|
+
description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
|
|
574
|
+
prefixes: ["AKIA", "ASIA"],
|
|
575
|
+
async validate(value) {
|
|
576
|
+
const start = Date.now();
|
|
577
|
+
const latencyMs = Date.now() - start;
|
|
578
|
+
if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
|
|
579
|
+
return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
|
|
580
|
+
}
|
|
581
|
+
return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var httpProvider = {
|
|
585
|
+
name: "http",
|
|
586
|
+
description: "Generic HTTP endpoint validation",
|
|
587
|
+
async validate(value, url) {
|
|
588
|
+
const start = Date.now();
|
|
589
|
+
if (!url) {
|
|
590
|
+
return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const { statusCode } = await makeRequest(url, {
|
|
594
|
+
Authorization: `Bearer ${value}`,
|
|
595
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
596
|
+
});
|
|
597
|
+
const latencyMs = Date.now() - start;
|
|
598
|
+
if (statusCode >= 200 && statusCode < 300)
|
|
599
|
+
return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
|
|
600
|
+
if (statusCode === 401 || statusCode === 403)
|
|
601
|
+
return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
|
|
602
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
var registry = new ProviderRegistry();
|
|
609
|
+
registry.register(openaiProvider);
|
|
610
|
+
registry.register(stripeProvider);
|
|
611
|
+
registry.register(githubProvider);
|
|
612
|
+
registry.register(awsProvider);
|
|
613
|
+
registry.register(httpProvider);
|
|
614
|
+
async function validateSecret(value, opts) {
|
|
615
|
+
const provider = opts?.provider ? registry.get(opts.provider) : registry.detectProvider(value);
|
|
616
|
+
if (!provider) {
|
|
617
|
+
return {
|
|
618
|
+
valid: false,
|
|
619
|
+
status: "unknown",
|
|
620
|
+
message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
|
|
621
|
+
latencyMs: 0,
|
|
622
|
+
provider: "none"
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (provider.name === "http" && opts?.validationUrl) {
|
|
626
|
+
return provider.validate(value, opts.validationUrl);
|
|
627
|
+
}
|
|
628
|
+
return provider.validate(value);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/cli/commands.ts
|
|
632
|
+
import { writeFileSync } from "fs";
|
|
633
|
+
|
|
356
634
|
// src/utils/prompt.ts
|
|
357
635
|
import { createInterface } from "readline";
|
|
358
636
|
async function promptSecret(message) {
|
|
@@ -392,6 +670,15 @@ async function promptSecret(message) {
|
|
|
392
670
|
}
|
|
393
671
|
|
|
394
672
|
// src/cli/commands.ts
|
|
673
|
+
function safeStr(s) {
|
|
674
|
+
return s == null ? "" : `${s}`;
|
|
675
|
+
}
|
|
676
|
+
function safeNum(n) {
|
|
677
|
+
return n == null ? 0 : Number(n);
|
|
678
|
+
}
|
|
679
|
+
function safeArr(arr) {
|
|
680
|
+
return arr ? arr.map((x) => typeof x === "string" ? safeStr(x) : x) : [];
|
|
681
|
+
}
|
|
395
682
|
function buildOpts(cmd) {
|
|
396
683
|
let scope;
|
|
397
684
|
if (cmd.global) scope = "global";
|
|
@@ -410,8 +697,8 @@ function buildOpts(cmd) {
|
|
|
410
697
|
function createProgram() {
|
|
411
698
|
const program2 = new Command().name("qring").description(
|
|
412
699
|
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
413
|
-
).version("0.
|
|
414
|
-
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").action(async (key, value, cmd) => {
|
|
700
|
+
).version("0.4.0");
|
|
701
|
+
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").option("--rotation-format <format>", "Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)").option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").action(async (key, value, cmd) => {
|
|
415
702
|
const opts = buildOpts(cmd);
|
|
416
703
|
if (!value) {
|
|
417
704
|
value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
|
|
@@ -425,7 +712,9 @@ function createProgram() {
|
|
|
425
712
|
ttlSeconds: cmd.ttl,
|
|
426
713
|
expiresAt: cmd.expires,
|
|
427
714
|
description: cmd.description,
|
|
428
|
-
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
715
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
716
|
+
rotationFormat: cmd.rotationFormat,
|
|
717
|
+
rotationPrefix: cmd.rotationPrefix
|
|
429
718
|
};
|
|
430
719
|
if (cmd.env) {
|
|
431
720
|
const existing = getEnvelope(key, opts);
|
|
@@ -469,9 +758,29 @@ function createProgram() {
|
|
|
469
758
|
process.exit(1);
|
|
470
759
|
}
|
|
471
760
|
});
|
|
472
|
-
program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").action((cmd) => {
|
|
761
|
+
program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").option("-t, --tag <tag>", "Filter by tag").option("--expired", "Show only expired secrets").option("--stale", "Show only stale secrets (75%+ decay)").option("-f, --filter <pattern>", "Glob pattern on key name").action((cmd) => {
|
|
473
762
|
const opts = buildOpts(cmd);
|
|
474
|
-
|
|
763
|
+
let entries = listSecrets(opts);
|
|
764
|
+
if (cmd.tag) {
|
|
765
|
+
entries = entries.filter(
|
|
766
|
+
(e) => e.envelope?.meta.tags?.includes(cmd.tag)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (cmd.expired) {
|
|
770
|
+
entries = entries.filter((e) => e.decay?.isExpired);
|
|
771
|
+
}
|
|
772
|
+
if (cmd.stale) {
|
|
773
|
+
entries = entries.filter(
|
|
774
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (cmd.filter) {
|
|
778
|
+
const regex = new RegExp(
|
|
779
|
+
"^" + cmd.filter.replace(/\*/g, ".*") + "$",
|
|
780
|
+
"i"
|
|
781
|
+
);
|
|
782
|
+
entries = entries.filter((e) => regex.test(e.key));
|
|
783
|
+
}
|
|
475
784
|
if (entries.length === 0) {
|
|
476
785
|
console.log(c.dim("No secrets found"));
|
|
477
786
|
return;
|
|
@@ -484,34 +793,34 @@ function createProgram() {
|
|
|
484
793
|
const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
|
|
485
794
|
for (const entry of entries) {
|
|
486
795
|
const parts = [];
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
796
|
+
const key = safeStr(entry.key);
|
|
797
|
+
const scope = safeStr(entry.scope);
|
|
798
|
+
const envs = entry.envelope?.states ? Object.keys(entry.envelope.states).map(safeStr) : null;
|
|
799
|
+
const entangledCount = safeNum(entry.envelope?.meta.entangled?.length);
|
|
800
|
+
const accessCount = safeNum(entry.envelope?.meta.accessCount);
|
|
801
|
+
const tags = safeArr(entry.envelope?.meta.tags);
|
|
802
|
+
const decayPct = safeNum(entry.decay?.lifetimePercent);
|
|
803
|
+
const expired = !!entry.decay?.isExpired;
|
|
804
|
+
const timeLeft = safeStr(entry.decay?.timeRemaining);
|
|
805
|
+
parts.push(c.dim("[") + scopeColor(scope) + c.dim("]"));
|
|
806
|
+
parts.push(c.bold(key.padEnd(maxKeyLen)));
|
|
807
|
+
if (envs) {
|
|
491
808
|
parts.push(c.magenta(`[${envs.join("|")}]`));
|
|
492
809
|
}
|
|
493
|
-
if (entry.decay && (
|
|
494
|
-
parts.push(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (entry.decay.timeRemaining && !entry.decay.isExpired) {
|
|
498
|
-
parts.push(c.dim(entry.decay.timeRemaining));
|
|
810
|
+
if (entry.decay && (decayPct > 0 || expired)) {
|
|
811
|
+
parts.push(decayIndicator(decayPct, expired));
|
|
812
|
+
if (timeLeft && !expired) {
|
|
813
|
+
parts.push(c.dim(timeLeft));
|
|
499
814
|
}
|
|
500
815
|
}
|
|
501
|
-
if (
|
|
502
|
-
parts.push(
|
|
503
|
-
c.cyan(`${SYMBOLS.link} ${entry.envelope.meta.entangled.length}`)
|
|
504
|
-
);
|
|
816
|
+
if (entangledCount > 0) {
|
|
817
|
+
parts.push(c.cyan(`${SYMBOLS.link} ${entangledCount}`));
|
|
505
818
|
}
|
|
506
|
-
if (
|
|
507
|
-
parts.push(
|
|
508
|
-
c.dim(`${SYMBOLS.eye} ${entry.envelope.meta.accessCount}`)
|
|
509
|
-
);
|
|
819
|
+
if (accessCount > 0) {
|
|
820
|
+
parts.push(c.dim(`${SYMBOLS.eye} ${accessCount}`));
|
|
510
821
|
}
|
|
511
|
-
if (
|
|
512
|
-
parts.push(
|
|
513
|
-
c.dim(entry.envelope.meta.tags.map((t) => `#${t}`).join(" "))
|
|
514
|
-
);
|
|
822
|
+
if (tags.length > 0) {
|
|
823
|
+
parts.push(c.dim(tags.map((t) => `#${t}`).join(" ")));
|
|
515
824
|
}
|
|
516
825
|
console.log(` ${parts.join(" ")}`);
|
|
517
826
|
}
|
|
@@ -526,14 +835,30 @@ function createProgram() {
|
|
|
526
835
|
}
|
|
527
836
|
const { envelope, scope } = result;
|
|
528
837
|
const decay = checkDecay(envelope);
|
|
838
|
+
const safeScope = safeStr(scope);
|
|
839
|
+
const createdAt = safeStr(envelope.meta.createdAt);
|
|
840
|
+
const updatedAt = safeStr(envelope.meta.updatedAt);
|
|
841
|
+
const accessCount = safeNum(envelope.meta.accessCount);
|
|
842
|
+
const lastAccess = safeStr(envelope.meta.lastAccessedAt);
|
|
843
|
+
const desc = safeStr(envelope.meta.description);
|
|
844
|
+
const tags = safeArr(envelope.meta.tags);
|
|
845
|
+
const entangled = (envelope.meta.entangled ?? []).map((l) => ({
|
|
846
|
+
service: safeStr(l.service),
|
|
847
|
+
key: safeStr(l.key)
|
|
848
|
+
}));
|
|
849
|
+
const stateEnvs = envelope.states ? Object.keys(envelope.states).map(safeStr) : null;
|
|
850
|
+
const defaultEnv = safeStr(envelope.defaultEnv);
|
|
851
|
+
const decayTime = safeStr(decay.timeRemaining);
|
|
852
|
+
const decayPct = safeNum(decay.lifetimePercent);
|
|
853
|
+
const expired = !!decay.isExpired;
|
|
529
854
|
console.log(`
|
|
530
855
|
${c.bold(SYMBOLS.key + " " + key)}`);
|
|
531
|
-
console.log(` ${c.dim("scope:")} ${scopeColor(
|
|
532
|
-
if (
|
|
856
|
+
console.log(` ${c.dim("scope:")} ${scopeColor(safeScope)}`);
|
|
857
|
+
if (stateEnvs) {
|
|
533
858
|
console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
|
|
534
859
|
console.log(` ${c.dim("states:")}`);
|
|
535
|
-
for (const
|
|
536
|
-
const isDefault = env ===
|
|
860
|
+
for (const env of stateEnvs) {
|
|
861
|
+
const isDefault = env === defaultEnv;
|
|
537
862
|
console.log(
|
|
538
863
|
` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
|
|
539
864
|
);
|
|
@@ -541,42 +866,227 @@ function createProgram() {
|
|
|
541
866
|
} else {
|
|
542
867
|
console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
|
|
543
868
|
}
|
|
544
|
-
console.log(` ${c.dim("created:")} ${
|
|
545
|
-
console.log(` ${c.dim("updated:")} ${
|
|
546
|
-
console.log(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (envelope.meta.lastAccessedAt) {
|
|
550
|
-
console.log(
|
|
551
|
-
` ${c.dim("last read:")} ${envelope.meta.lastAccessedAt}`
|
|
552
|
-
);
|
|
869
|
+
console.log(` ${c.dim("created:")} ${createdAt}`);
|
|
870
|
+
console.log(` ${c.dim("updated:")} ${updatedAt}`);
|
|
871
|
+
console.log(` ${c.dim("accessed:")} ${accessCount} times`);
|
|
872
|
+
if (lastAccess) {
|
|
873
|
+
console.log(` ${c.dim("last read:")} ${lastAccess}`);
|
|
553
874
|
}
|
|
554
|
-
if (
|
|
555
|
-
console.log(` ${c.dim("desc:")} ${
|
|
875
|
+
if (desc) {
|
|
876
|
+
console.log(` ${c.dim("desc:")} ${desc}`);
|
|
556
877
|
}
|
|
557
|
-
if (
|
|
878
|
+
if (tags.length > 0) {
|
|
558
879
|
console.log(
|
|
559
|
-
` ${c.dim("tags:")} ${
|
|
880
|
+
` ${c.dim("tags:")} ${tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
|
|
560
881
|
);
|
|
561
882
|
}
|
|
562
|
-
if (
|
|
883
|
+
if (decayTime) {
|
|
563
884
|
console.log(
|
|
564
|
-
` ${c.dim("decay:")} ${decayIndicator(
|
|
885
|
+
` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
|
|
565
886
|
);
|
|
566
887
|
}
|
|
567
|
-
if (
|
|
888
|
+
if (entangled.length > 0) {
|
|
568
889
|
console.log(` ${c.dim("entangled:")}`);
|
|
569
|
-
for (const link of
|
|
890
|
+
for (const link of entangled) {
|
|
570
891
|
console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
|
|
571
892
|
}
|
|
572
893
|
}
|
|
573
894
|
console.log();
|
|
574
895
|
});
|
|
575
|
-
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").action((cmd) => {
|
|
896
|
+
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").option("-k, --keys <keys>", "Comma-separated key names to export").option("-t, --tags <tags>", "Comma-separated tags to filter by").action((cmd) => {
|
|
576
897
|
const opts = buildOpts(cmd);
|
|
577
|
-
const output = exportSecrets({
|
|
898
|
+
const output = exportSecrets({
|
|
899
|
+
...opts,
|
|
900
|
+
format: cmd.format,
|
|
901
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
902
|
+
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
903
|
+
});
|
|
578
904
|
process.stdout.write(output + "\n");
|
|
579
905
|
});
|
|
906
|
+
program2.command("import <file>").description("Import secrets from a .env file").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--skip-existing", "Skip keys that already exist").option("--dry-run", "Preview what would be imported without saving").action((file, cmd) => {
|
|
907
|
+
const opts = buildOpts(cmd);
|
|
908
|
+
const result = importDotenv(file, {
|
|
909
|
+
scope: opts.scope,
|
|
910
|
+
projectPath: opts.projectPath,
|
|
911
|
+
source: "cli",
|
|
912
|
+
skipExisting: cmd.skipExisting,
|
|
913
|
+
dryRun: cmd.dryRun
|
|
914
|
+
});
|
|
915
|
+
if (cmd.dryRun) {
|
|
916
|
+
console.log(
|
|
917
|
+
`
|
|
918
|
+
${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
|
|
919
|
+
`
|
|
920
|
+
);
|
|
921
|
+
for (const key of result.imported) {
|
|
922
|
+
console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
|
|
923
|
+
}
|
|
924
|
+
if (result.skipped.length > 0) {
|
|
925
|
+
console.log(`
|
|
926
|
+
${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
console.log(
|
|
930
|
+
`${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
|
|
931
|
+
);
|
|
932
|
+
if (result.skipped.length > 0) {
|
|
933
|
+
console.log(
|
|
934
|
+
c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
console.log();
|
|
939
|
+
});
|
|
940
|
+
program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
941
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
942
|
+
const config = readProjectConfig(projectPath);
|
|
943
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
944
|
+
console.error(
|
|
945
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
946
|
+
);
|
|
947
|
+
console.log(
|
|
948
|
+
c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
|
|
949
|
+
);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
console.log(
|
|
953
|
+
c.bold(`
|
|
954
|
+
${SYMBOLS.shield} Project secret manifest check
|
|
955
|
+
`)
|
|
956
|
+
);
|
|
957
|
+
let present = 0;
|
|
958
|
+
let missing = 0;
|
|
959
|
+
let expiredCount = 0;
|
|
960
|
+
let staleCount = 0;
|
|
961
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
962
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
963
|
+
if (!result) {
|
|
964
|
+
if (manifest.required !== false) {
|
|
965
|
+
missing++;
|
|
966
|
+
console.log(
|
|
967
|
+
` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
968
|
+
);
|
|
969
|
+
} else {
|
|
970
|
+
console.log(
|
|
971
|
+
` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const decay = checkDecay(result.envelope);
|
|
977
|
+
if (decay.isExpired) {
|
|
978
|
+
expiredCount++;
|
|
979
|
+
console.log(
|
|
980
|
+
` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
981
|
+
);
|
|
982
|
+
} else if (decay.isStale) {
|
|
983
|
+
staleCount++;
|
|
984
|
+
console.log(
|
|
985
|
+
` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
986
|
+
);
|
|
987
|
+
} else {
|
|
988
|
+
present++;
|
|
989
|
+
console.log(
|
|
990
|
+
` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const total = Object.keys(config.secrets).length;
|
|
995
|
+
console.log(
|
|
996
|
+
`
|
|
997
|
+
${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
|
|
998
|
+
);
|
|
999
|
+
if (missing > 0) {
|
|
1000
|
+
console.log(
|
|
1001
|
+
`
|
|
1002
|
+
${c.red("Project is NOT ready \u2014 missing required secrets.")}`
|
|
1003
|
+
);
|
|
1004
|
+
} else if (expiredCount > 0) {
|
|
1005
|
+
console.log(
|
|
1006
|
+
`
|
|
1007
|
+
${c.yellow("Project has expired secrets that need rotation.")}`
|
|
1008
|
+
);
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(
|
|
1011
|
+
`
|
|
1012
|
+
${c.green(`${SYMBOLS.check} Project is ready \u2014 all required secrets present.`)}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
console.log();
|
|
1016
|
+
if (missing > 0) process.exit(1);
|
|
1017
|
+
});
|
|
1018
|
+
program2.command("validate [key]").description("Test if a secret is actually valid with its target service").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider (openai, stripe, github, aws, http)").option("--all", "Validate all secrets that have a detectable provider").option("--manifest", "Only validate manifest-declared secrets (with --all)").option("--list-providers", "List all available providers").action(async (key, cmd) => {
|
|
1019
|
+
if (cmd.listProviders) {
|
|
1020
|
+
console.log(c.bold(`
|
|
1021
|
+
${SYMBOLS.shield} Available validation providers
|
|
1022
|
+
`));
|
|
1023
|
+
for (const p of registry.listProviders()) {
|
|
1024
|
+
const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
|
|
1025
|
+
console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
|
|
1026
|
+
}
|
|
1027
|
+
console.log();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (!key && !cmd.all) {
|
|
1031
|
+
console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
const opts = buildOpts(cmd);
|
|
1035
|
+
if (cmd.all) {
|
|
1036
|
+
let entries = listSecrets(opts);
|
|
1037
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1038
|
+
if (cmd.manifest) {
|
|
1039
|
+
const config = readProjectConfig(projectPath);
|
|
1040
|
+
if (config?.secrets) {
|
|
1041
|
+
const manifestKeys = new Set(Object.keys(config.secrets));
|
|
1042
|
+
entries = entries.filter((e) => manifestKeys.has(e.key));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
console.log(c.bold(`
|
|
1046
|
+
${SYMBOLS.shield} Validating secrets
|
|
1047
|
+
`));
|
|
1048
|
+
let validated = 0;
|
|
1049
|
+
let skipped = 0;
|
|
1050
|
+
for (const entry of entries) {
|
|
1051
|
+
const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1052
|
+
if (!value2) {
|
|
1053
|
+
skipped++;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
|
|
1057
|
+
const result2 = await validateSecret(value2, { provider: provHint2 });
|
|
1058
|
+
if (result2.status === "unknown") {
|
|
1059
|
+
skipped++;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
validated++;
|
|
1063
|
+
const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
|
|
1064
|
+
const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
|
|
1065
|
+
console.log(
|
|
1066
|
+
` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
console.log(`
|
|
1070
|
+
${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
|
|
1071
|
+
`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const value = getSecret(key, opts);
|
|
1075
|
+
if (!value) {
|
|
1076
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
const envelope = getEnvelope(key, opts);
|
|
1080
|
+
const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
|
|
1081
|
+
const result = await validateSecret(value, { provider: provHint });
|
|
1082
|
+
const icon = result.status === "valid" ? c.green(SYMBOLS.check) : result.status === "invalid" ? c.red(SYMBOLS.cross) : result.status === "error" ? c.yellow(SYMBOLS.warning) : c.dim("\u25CB");
|
|
1083
|
+
console.log(`
|
|
1084
|
+
${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
|
|
1085
|
+
if (result.message && result.status !== "valid") {
|
|
1086
|
+
console.log(` ${c.dim(result.message)}`);
|
|
1087
|
+
}
|
|
1088
|
+
console.log();
|
|
1089
|
+
});
|
|
580
1090
|
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
581
1091
|
const result = collapseEnvironment({
|
|
582
1092
|
projectPath: cmd.projectPath ?? process.cwd()
|
|
@@ -636,6 +1146,24 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
636
1146
|
);
|
|
637
1147
|
}
|
|
638
1148
|
);
|
|
1149
|
+
program2.command("disentangle <sourceKey> <targetKey>").description("Unlink two entangled secrets").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
|
|
1150
|
+
(sourceKey, targetKey, cmd) => {
|
|
1151
|
+
const sourceOpts = {
|
|
1152
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
1153
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
1154
|
+
source: "cli"
|
|
1155
|
+
};
|
|
1156
|
+
const targetOpts = {
|
|
1157
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
1158
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
1159
|
+
source: "cli"
|
|
1160
|
+
};
|
|
1161
|
+
disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
1162
|
+
console.log(
|
|
1163
|
+
`${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
639
1167
|
const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
|
|
640
1168
|
tunnel.command("create <value>").description("Create a tunneled secret (returns tunnel ID)").option("--ttl <seconds>", "Auto-expire after N seconds", parseInt).option("--max-reads <n>", "Self-destruct after N reads", parseInt).action((value, cmd) => {
|
|
641
1169
|
const id = tunnelCreate(value, {
|
|
@@ -863,8 +1391,166 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
863
1391
|
}
|
|
864
1392
|
console.log();
|
|
865
1393
|
});
|
|
1394
|
+
const hook = program2.command("hook").description("Manage secret change hooks (callbacks on write/delete/rotate)");
|
|
1395
|
+
hook.command("add").description("Register a new hook").option("--key <key>", "Trigger on exact key match").option("--key-pattern <pattern>", "Trigger on key glob pattern (e.g. DB_*)").option("--tag <tag>", "Trigger on secrets with this tag").option("--scope <scope>", "Trigger only for this scope (global or project)").option("--action <actions>", "Comma-separated actions: write,delete,rotate", "write,delete,rotate").option("--exec <command>", "Shell command to execute").option("--url <url>", "HTTP URL to POST to").option("--signal-target <target>", "Process name or PID to signal").option("--signal-name <signal>", "Signal to send (default: SIGHUP)", "SIGHUP").option("--description <desc>", "Human-readable description").action((cmd) => {
|
|
1396
|
+
let type;
|
|
1397
|
+
if (cmd.exec) type = "shell";
|
|
1398
|
+
else if (cmd.url) type = "http";
|
|
1399
|
+
else if (cmd.signalTarget) type = "signal";
|
|
1400
|
+
else {
|
|
1401
|
+
console.error(c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`));
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
|
|
1405
|
+
console.error(c.red(`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`));
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
const actions = cmd.action.split(",").map((a) => a.trim());
|
|
1409
|
+
const entry = registerHook({
|
|
1410
|
+
type,
|
|
1411
|
+
match: {
|
|
1412
|
+
key: cmd.key,
|
|
1413
|
+
keyPattern: cmd.keyPattern,
|
|
1414
|
+
tag: cmd.tag,
|
|
1415
|
+
scope: cmd.scope,
|
|
1416
|
+
action: actions
|
|
1417
|
+
},
|
|
1418
|
+
command: cmd.exec,
|
|
1419
|
+
url: cmd.url,
|
|
1420
|
+
signal: cmd.signalTarget ? { target: cmd.signalTarget, signal: cmd.signalName } : void 0,
|
|
1421
|
+
description: cmd.description,
|
|
1422
|
+
enabled: true
|
|
1423
|
+
});
|
|
1424
|
+
console.log(`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`);
|
|
1425
|
+
if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
|
|
1426
|
+
if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
|
|
1427
|
+
if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
|
|
1428
|
+
});
|
|
1429
|
+
hook.command("list").alias("ls").description("List all registered hooks").action(() => {
|
|
1430
|
+
const hooks = listHooks();
|
|
1431
|
+
if (hooks.length === 0) {
|
|
1432
|
+
console.log(c.dim("No hooks registered"));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
console.log(c.bold(`
|
|
1436
|
+
${SYMBOLS.zap} Registered hooks (${hooks.length})
|
|
1437
|
+
`));
|
|
1438
|
+
for (const h of hooks) {
|
|
1439
|
+
const status = h.enabled ? c.green("on") : c.red("off");
|
|
1440
|
+
const matchParts = [];
|
|
1441
|
+
if (h.match.key) matchParts.push(`key=${h.match.key}`);
|
|
1442
|
+
if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
|
|
1443
|
+
if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
|
|
1444
|
+
if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
|
|
1445
|
+
if (h.match.action?.length) matchParts.push(`actions=${h.match.action.join(",")}`);
|
|
1446
|
+
const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
|
|
1447
|
+
console.log(` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`);
|
|
1448
|
+
console.log(` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`);
|
|
1449
|
+
}
|
|
1450
|
+
console.log();
|
|
1451
|
+
});
|
|
1452
|
+
hook.command("remove <id>").alias("rm").description("Remove a hook by ID").action((id) => {
|
|
1453
|
+
if (removeHook(id)) {
|
|
1454
|
+
console.log(`${SYMBOLS.check} ${c.green("removed")} hook ${c.bold(id)}`);
|
|
1455
|
+
} else {
|
|
1456
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
hook.command("enable <id>").description("Enable a hook").action((id) => {
|
|
1461
|
+
if (enableHook(id)) {
|
|
1462
|
+
console.log(`${SYMBOLS.check} ${c.green("enabled")} hook ${c.bold(id)}`);
|
|
1463
|
+
} else {
|
|
1464
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
hook.command("disable <id>").description("Disable a hook").action((id) => {
|
|
1469
|
+
if (disableHook(id)) {
|
|
1470
|
+
console.log(`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`);
|
|
1471
|
+
} else {
|
|
1472
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
hook.command("test <id>").description("Dry-run a hook with a mock payload").action(async (id) => {
|
|
1477
|
+
const hooks = listHooks();
|
|
1478
|
+
const h = hooks.find((hook2) => hook2.id === id);
|
|
1479
|
+
if (!h) {
|
|
1480
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
console.log(c.dim(`Testing hook ${id} (${h.type})...
|
|
1484
|
+
`));
|
|
1485
|
+
const payload = {
|
|
1486
|
+
action: "write",
|
|
1487
|
+
key: h.match.key ?? "TEST_KEY",
|
|
1488
|
+
scope: h.match.scope ?? "global",
|
|
1489
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1490
|
+
source: "cli"
|
|
1491
|
+
};
|
|
1492
|
+
const results = await fireHooks(payload);
|
|
1493
|
+
const result = results.find((r) => r.hookId === id);
|
|
1494
|
+
if (result) {
|
|
1495
|
+
const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
|
|
1496
|
+
console.log(` ${icon} ${result.message}`);
|
|
1497
|
+
} else {
|
|
1498
|
+
console.log(c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`));
|
|
1499
|
+
}
|
|
1500
|
+
console.log();
|
|
1501
|
+
});
|
|
1502
|
+
program2.command("env:generate").description("Generate a .env file from the project manifest (.q-ring.json)").option("--project-path <path>", "Project path (defaults to cwd)").option("-o, --output <file>", "Output file path (defaults to stdout)").option("-e, --env <env>", "Force environment for superposition collapse").action((cmd) => {
|
|
1503
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1504
|
+
const config = readProjectConfig(projectPath);
|
|
1505
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1506
|
+
console.error(
|
|
1507
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
1508
|
+
);
|
|
1509
|
+
process.exit(1);
|
|
1510
|
+
}
|
|
1511
|
+
const opts = buildOpts(cmd);
|
|
1512
|
+
const lines = [];
|
|
1513
|
+
const warnings = [];
|
|
1514
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1515
|
+
const value = getSecret(key, { ...opts, projectPath, source: "cli" });
|
|
1516
|
+
if (value === null) {
|
|
1517
|
+
if (manifest.required !== false) {
|
|
1518
|
+
warnings.push(`MISSING (required): ${key}`);
|
|
1519
|
+
}
|
|
1520
|
+
lines.push(`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`);
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
1524
|
+
if (result) {
|
|
1525
|
+
const decay = checkDecay(result.envelope);
|
|
1526
|
+
if (decay.isExpired) {
|
|
1527
|
+
warnings.push(`EXPIRED: ${key}`);
|
|
1528
|
+
} else if (decay.isStale) {
|
|
1529
|
+
warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1533
|
+
lines.push(`${key}="${escaped}"`);
|
|
1534
|
+
}
|
|
1535
|
+
const output = lines.join("\n") + "\n";
|
|
1536
|
+
if (cmd.output) {
|
|
1537
|
+
writeFileSync(cmd.output, output);
|
|
1538
|
+
console.log(
|
|
1539
|
+
`${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
|
|
1540
|
+
);
|
|
1541
|
+
} else {
|
|
1542
|
+
process.stdout.write(output);
|
|
1543
|
+
}
|
|
1544
|
+
if (warnings.length > 0 && process.stderr.isTTY) {
|
|
1545
|
+
console.error();
|
|
1546
|
+
for (const w of warnings) {
|
|
1547
|
+
console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
|
|
1548
|
+
}
|
|
1549
|
+
console.error();
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
866
1552
|
program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
|
|
867
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
1553
|
+
const { startDashboardServer } = await import("./dashboard-HVIQO6NT.js");
|
|
868
1554
|
const { exec } = await import("child_process");
|
|
869
1555
|
const { platform } = await import("os");
|
|
870
1556
|
const port = Number(cmd.port);
|