@i4ctime/q-ring 0.3.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,22 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  checkDecay,
4
+ checkExecPolicy,
4
5
  collapseEnvironment,
5
6
  deleteSecret,
6
7
  detectAnomalies,
8
+ disableHook,
9
+ disentangleSecrets,
10
+ enableHook,
7
11
  entangleSecrets,
12
+ exportAudit,
8
13
  exportSecrets,
14
+ fireHooks,
9
15
  getEnvelope,
16
+ getExecMaxRuntime,
17
+ getPolicySummary,
10
18
  getSecret,
19
+ grantApproval,
20
+ hasSecret,
21
+ httpRequest_,
22
+ listApprovals,
23
+ listHooks,
11
24
  listSecrets,
12
25
  logAudit,
13
26
  queryAudit,
27
+ readProjectConfig,
28
+ registerHook,
29
+ registry,
30
+ removeHook,
31
+ revokeApproval,
14
32
  setSecret,
15
33
  tunnelCreate,
16
34
  tunnelDestroy,
17
35
  tunnelList,
18
- tunnelRead
19
- } from "./chunk-F4SPZ774.js";
36
+ tunnelRead,
37
+ verifyAuditChain
38
+ } from "./chunk-WG4ZKN7Q.js";
20
39
 
21
40
  // src/cli/commands.ts
22
41
  import { Command } from "commander";
@@ -207,7 +226,9 @@ function runHealthScan(config = {}) {
207
226
  `EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
208
227
  );
209
228
  if (cfg.autoRotate) {
210
- const newValue = generateSecret({ format: "api-key" });
229
+ const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
230
+ const prefix = entry.envelope?.meta.rotationPrefix;
231
+ const newValue = generateSecret({ format: fmt, prefix });
211
232
  setSecret(entry.key, newValue, {
212
233
  scope: entry.scope,
213
234
  projectPath: cfg.projectPaths[0],
@@ -221,6 +242,14 @@ function runHealthScan(config = {}) {
221
242
  source: "agent",
222
243
  detail: "auto-rotated by agent (expired)"
223
244
  });
245
+ fireHooks({
246
+ action: "rotate",
247
+ key: entry.key,
248
+ scope: entry.scope,
249
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
250
+ source: "agent"
251
+ }, entry.envelope?.meta.tags).catch(() => {
252
+ });
224
253
  }
225
254
  } else if (decay.isStale) {
226
255
  report.stale++;
@@ -353,6 +382,937 @@ function teleportUnpack(encoded, passphrase) {
353
382
  return JSON.parse(decrypted.toString("utf8"));
354
383
  }
355
384
 
385
+ // src/core/import.ts
386
+ import { readFileSync } from "fs";
387
+ function parseDotenv(content) {
388
+ const result = /* @__PURE__ */ new Map();
389
+ const lines = content.split(/\r?\n/);
390
+ for (let i = 0; i < lines.length; i++) {
391
+ const line = lines[i].trim();
392
+ if (!line || line.startsWith("#")) continue;
393
+ const eqIdx = line.indexOf("=");
394
+ if (eqIdx === -1) continue;
395
+ const key = line.slice(0, eqIdx).trim();
396
+ let value = line.slice(eqIdx + 1).trim();
397
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
398
+ value = value.slice(1, -1);
399
+ }
400
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
401
+ if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
402
+ value = value.split("#")[0].trim();
403
+ }
404
+ if (key) result.set(key, value);
405
+ }
406
+ return result;
407
+ }
408
+ function importDotenv(filePathOrContent, options = {}) {
409
+ let content;
410
+ try {
411
+ content = readFileSync(filePathOrContent, "utf8");
412
+ } catch {
413
+ content = filePathOrContent;
414
+ }
415
+ const pairs = parseDotenv(content);
416
+ const result = {
417
+ imported: [],
418
+ skipped: [],
419
+ total: pairs.size
420
+ };
421
+ for (const [key, value] of pairs) {
422
+ if (options.skipExisting && hasSecret(key, {
423
+ scope: options.scope,
424
+ projectPath: options.projectPath,
425
+ source: options.source ?? "cli"
426
+ })) {
427
+ result.skipped.push(key);
428
+ continue;
429
+ }
430
+ if (options.dryRun) {
431
+ result.imported.push(key);
432
+ continue;
433
+ }
434
+ const setOpts = {
435
+ scope: options.scope ?? "global",
436
+ projectPath: options.projectPath ?? process.cwd(),
437
+ source: options.source ?? "cli"
438
+ };
439
+ setSecret(key, value, setOpts);
440
+ result.imported.push(key);
441
+ }
442
+ return result;
443
+ }
444
+
445
+ // src/core/exec.ts
446
+ import { spawn } from "child_process";
447
+ import { Transform } from "stream";
448
+ var BUILTIN_PROFILES = {
449
+ unrestricted: { name: "unrestricted" },
450
+ restricted: {
451
+ name: "restricted",
452
+ denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
453
+ maxRuntimeSeconds: 30,
454
+ allowNetwork: false,
455
+ stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
456
+ },
457
+ ci: {
458
+ name: "ci",
459
+ maxRuntimeSeconds: 300,
460
+ allowNetwork: true,
461
+ denyCommands: ["rm -rf /", "mkfs", "dd if="]
462
+ }
463
+ };
464
+ function getProfile(name) {
465
+ if (!name) return BUILTIN_PROFILES.unrestricted;
466
+ return BUILTIN_PROFILES[name] ?? { name };
467
+ }
468
+ var RedactionTransform = class extends Transform {
469
+ patterns = [];
470
+ tail = "";
471
+ maxLen = 0;
472
+ constructor(secretsToRedact) {
473
+ super();
474
+ const validSecrets = secretsToRedact.filter((s) => s.length > 5);
475
+ validSecrets.sort((a, b) => b.length - a.length);
476
+ this.patterns = validSecrets.map((s) => ({
477
+ value: s,
478
+ replacement: "[QRING:REDACTED]"
479
+ }));
480
+ if (validSecrets.length > 0) {
481
+ this.maxLen = validSecrets[0].length;
482
+ }
483
+ }
484
+ _transform(chunk, encoding, callback) {
485
+ if (this.patterns.length === 0) {
486
+ this.push(chunk);
487
+ return callback();
488
+ }
489
+ const text = this.tail + chunk.toString();
490
+ let redacted = text;
491
+ for (const { value, replacement } of this.patterns) {
492
+ redacted = redacted.split(value).join(replacement);
493
+ }
494
+ if (redacted.length < this.maxLen) {
495
+ this.tail = redacted;
496
+ return callback();
497
+ }
498
+ const outputLen = redacted.length - this.maxLen + 1;
499
+ const output = redacted.slice(0, outputLen);
500
+ this.tail = redacted.slice(outputLen);
501
+ this.push(output);
502
+ callback();
503
+ }
504
+ _flush(callback) {
505
+ if (this.tail) {
506
+ let final = this.tail;
507
+ for (const { value, replacement } of this.patterns) {
508
+ final = final.split(value).join(replacement);
509
+ }
510
+ this.push(final);
511
+ }
512
+ callback();
513
+ }
514
+ };
515
+ async function execCommand(opts) {
516
+ const profile = getProfile(opts.profile);
517
+ const fullCommand = [opts.command, ...opts.args].join(" ");
518
+ const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
519
+ if (!policyDecision.allowed) {
520
+ throw new Error(`Policy Denied: ${policyDecision.reason}`);
521
+ }
522
+ if (profile.denyCommands) {
523
+ const denied = profile.denyCommands.find((d) => fullCommand.includes(d));
524
+ if (denied) {
525
+ throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
526
+ }
527
+ }
528
+ if (profile.allowCommands) {
529
+ const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
530
+ if (!allowed) {
531
+ throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
532
+ }
533
+ }
534
+ const envMap = {};
535
+ for (const [k, v] of Object.entries(process.env)) {
536
+ if (v !== void 0) envMap[k] = v;
537
+ }
538
+ if (profile.stripEnvVars) {
539
+ for (const key of profile.stripEnvVars) {
540
+ delete envMap[key];
541
+ }
542
+ }
543
+ const secretsToRedact = /* @__PURE__ */ new Set();
544
+ let entries = listSecrets({
545
+ scope: opts.scope,
546
+ projectPath: opts.projectPath,
547
+ source: opts.source ?? "cli",
548
+ silent: true
549
+ // list silently
550
+ });
551
+ if (opts.keys?.length) {
552
+ const keySet = new Set(opts.keys);
553
+ entries = entries.filter((e) => keySet.has(e.key));
554
+ }
555
+ if (opts.tags?.length) {
556
+ entries = entries.filter(
557
+ (e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
558
+ );
559
+ }
560
+ for (const entry of entries) {
561
+ if (entry.envelope) {
562
+ const decay = checkDecay(entry.envelope);
563
+ if (decay.isExpired) continue;
564
+ }
565
+ const val = getSecret(entry.key, {
566
+ scope: entry.scope,
567
+ projectPath: opts.projectPath,
568
+ env: opts.env,
569
+ source: opts.source ?? "cli",
570
+ silent: false
571
+ // Log access for execution
572
+ });
573
+ if (val !== null) {
574
+ envMap[entry.key] = val;
575
+ if (val.length > 5) {
576
+ secretsToRedact.add(val);
577
+ }
578
+ }
579
+ }
580
+ const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
581
+ return new Promise((resolve, reject) => {
582
+ const networkTools = /* @__PURE__ */ new Set([
583
+ "curl",
584
+ "wget",
585
+ "ping",
586
+ "nc",
587
+ "netcat",
588
+ "ssh",
589
+ "telnet",
590
+ "ftp",
591
+ "dig",
592
+ "nslookup"
593
+ ]);
594
+ if (profile.allowNetwork === false && networkTools.has(opts.command)) {
595
+ const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
596
+ if (opts.captureOutput) {
597
+ return resolve({ code: 126, stdout: "", stderr: msg });
598
+ }
599
+ process.stderr.write(msg + "\n");
600
+ return resolve({ code: 126, stdout: "", stderr: "" });
601
+ }
602
+ const child = spawn(opts.command, opts.args, {
603
+ env: envMap,
604
+ stdio: ["inherit", "pipe", "pipe"],
605
+ shell: false
606
+ });
607
+ let timedOut = false;
608
+ let timer;
609
+ if (maxRuntime) {
610
+ timer = setTimeout(() => {
611
+ timedOut = true;
612
+ child.kill("SIGKILL");
613
+ }, maxRuntime * 1e3);
614
+ }
615
+ const stdoutRedact = new RedactionTransform([...secretsToRedact]);
616
+ const stderrRedact = new RedactionTransform([...secretsToRedact]);
617
+ if (child.stdout) child.stdout.pipe(stdoutRedact);
618
+ if (child.stderr) child.stderr.pipe(stderrRedact);
619
+ let stdoutStr = "";
620
+ let stderrStr = "";
621
+ if (opts.captureOutput) {
622
+ stdoutRedact.on("data", (d) => stdoutStr += d.toString());
623
+ stderrRedact.on("data", (d) => stderrStr += d.toString());
624
+ } else {
625
+ stdoutRedact.pipe(process.stdout);
626
+ stderrRedact.pipe(process.stderr);
627
+ }
628
+ child.on("close", (code) => {
629
+ if (timer) clearTimeout(timer);
630
+ if (timedOut) {
631
+ resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
632
+ [QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
633
+ } else {
634
+ resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
635
+ }
636
+ });
637
+ child.on("error", (err) => {
638
+ if (timer) clearTimeout(timer);
639
+ reject(err);
640
+ });
641
+ });
642
+ }
643
+
644
+ // src/core/scan.ts
645
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
646
+ import { join } from "path";
647
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
648
+ "node_modules",
649
+ ".git",
650
+ ".next",
651
+ "dist",
652
+ "build",
653
+ "coverage",
654
+ ".cursor",
655
+ "venv",
656
+ "__pycache__"
657
+ ]);
658
+ var IGNORE_EXTS = /* @__PURE__ */ new Set([
659
+ ".png",
660
+ ".jpg",
661
+ ".jpeg",
662
+ ".gif",
663
+ ".ico",
664
+ ".svg",
665
+ ".webp",
666
+ ".mp4",
667
+ ".mp3",
668
+ ".wav",
669
+ ".ogg",
670
+ ".pdf",
671
+ ".zip",
672
+ ".tar",
673
+ ".gz",
674
+ ".xz",
675
+ ".ttf",
676
+ ".woff",
677
+ ".woff2",
678
+ ".eot",
679
+ ".exe",
680
+ ".dll",
681
+ ".so",
682
+ ".dylib",
683
+ ".lock"
684
+ ]);
685
+ var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
686
+ function calculateEntropy(str) {
687
+ if (!str) return 0;
688
+ const len = str.length;
689
+ const frequencies = /* @__PURE__ */ new Map();
690
+ for (let i = 0; i < len; i++) {
691
+ const char = str[i];
692
+ frequencies.set(char, (frequencies.get(char) || 0) + 1);
693
+ }
694
+ let entropy = 0;
695
+ for (const count of frequencies.values()) {
696
+ const p = count / len;
697
+ entropy -= p * Math.log2(p);
698
+ }
699
+ return entropy;
700
+ }
701
+ function scanCodebase(dir) {
702
+ const results = [];
703
+ function walk(currentDir) {
704
+ let entries;
705
+ try {
706
+ entries = readdirSync(currentDir);
707
+ } catch {
708
+ return;
709
+ }
710
+ for (const entry of entries) {
711
+ if (IGNORE_DIRS.has(entry)) continue;
712
+ const fullPath = join(currentDir, entry);
713
+ let stat;
714
+ try {
715
+ stat = statSync(fullPath);
716
+ } catch {
717
+ continue;
718
+ }
719
+ if (stat.isDirectory()) {
720
+ walk(fullPath);
721
+ } else if (stat.isFile()) {
722
+ const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
723
+ if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
724
+ let content;
725
+ try {
726
+ content = readFileSync2(fullPath, "utf8");
727
+ } catch {
728
+ continue;
729
+ }
730
+ if (content.includes("\0")) continue;
731
+ const lines = content.split(/\r?\n/);
732
+ for (let i = 0; i < lines.length; i++) {
733
+ const line = lines[i];
734
+ if (line.length > 500) continue;
735
+ const match = line.match(SECRET_KEYWORDS);
736
+ if (match) {
737
+ const varName = match[1];
738
+ const value = match[3];
739
+ if (value.length < 8) continue;
740
+ const lowerValue = value.toLowerCase();
741
+ if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
742
+ continue;
743
+ }
744
+ const entropy = calculateEntropy(value);
745
+ if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
746
+ const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
747
+ results.push({
748
+ file: relPath || fullPath,
749
+ line: i + 1,
750
+ keyName: varName,
751
+ match: value,
752
+ context: line.trim(),
753
+ entropy: parseFloat(entropy.toFixed(2))
754
+ });
755
+ }
756
+ }
757
+ }
758
+ }
759
+ }
760
+ }
761
+ walk(dir);
762
+ return results;
763
+ }
764
+
765
+ // src/core/linter.ts
766
+ import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
767
+ import { basename, extname } from "path";
768
+ var ENV_REF_BY_EXT = {
769
+ ".ts": (k) => `process.env.${k}`,
770
+ ".tsx": (k) => `process.env.${k}`,
771
+ ".js": (k) => `process.env.${k}`,
772
+ ".jsx": (k) => `process.env.${k}`,
773
+ ".mjs": (k) => `process.env.${k}`,
774
+ ".cjs": (k) => `process.env.${k}`,
775
+ ".py": (k) => `os.environ["${k}"]`,
776
+ ".rb": (k) => `ENV["${k}"]`,
777
+ ".go": (k) => `os.Getenv("${k}")`,
778
+ ".rs": (k) => `std::env::var("${k}")`,
779
+ ".java": (k) => `System.getenv("${k}")`,
780
+ ".kt": (k) => `System.getenv("${k}")`,
781
+ ".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
782
+ ".php": (k) => `getenv('${k}')`,
783
+ ".sh": (k) => `\${${k}}`,
784
+ ".bash": (k) => `\${${k}}`
785
+ };
786
+ function getEnvRef(filePath, keyName) {
787
+ const ext = extname(filePath).toLowerCase();
788
+ const formatter = ENV_REF_BY_EXT[ext];
789
+ return formatter ? formatter(keyName) : `process.env.${keyName}`;
790
+ }
791
+ function lintFiles(files, opts = {}) {
792
+ const results = [];
793
+ for (const file of files) {
794
+ if (!existsSync(file)) continue;
795
+ let content;
796
+ try {
797
+ content = readFileSync3(file, "utf8");
798
+ } catch {
799
+ continue;
800
+ }
801
+ if (content.includes("\0")) continue;
802
+ const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
803
+ const lines = content.split(/\r?\n/);
804
+ const fixes = [];
805
+ for (let i = 0; i < lines.length; i++) {
806
+ const line = lines[i];
807
+ if (line.length > 500) continue;
808
+ let match;
809
+ SECRET_KEYWORDS2.lastIndex = 0;
810
+ while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
811
+ const varName = match[1].toUpperCase();
812
+ const quote = match[2];
813
+ const value = match[3];
814
+ if (value.length < 8) continue;
815
+ const lv = value.toLowerCase();
816
+ if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
817
+ const entropy = calculateEntropy2(value);
818
+ if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
819
+ const shouldFix = opts.fix === true;
820
+ if (shouldFix) {
821
+ const envRef = getEnvRef(file, varName);
822
+ fixes.push({
823
+ line: i,
824
+ original: `${quote}${value}${quote}`,
825
+ replacement: envRef,
826
+ keyName: varName,
827
+ value
828
+ });
829
+ }
830
+ results.push({
831
+ file,
832
+ line: i + 1,
833
+ keyName: varName,
834
+ match: value,
835
+ context: line.trim(),
836
+ entropy: parseFloat(entropy.toFixed(2)),
837
+ fixed: shouldFix
838
+ });
839
+ }
840
+ }
841
+ if (opts.fix && fixes.length > 0) {
842
+ const fixLines = content.split(/\r?\n/);
843
+ for (const fix of fixes.reverse()) {
844
+ const lineIdx = fix.line;
845
+ if (lineIdx >= 0 && lineIdx < fixLines.length) {
846
+ fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
847
+ }
848
+ if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
849
+ setSecret(fix.keyName, fix.value, {
850
+ scope: opts.scope ?? "global",
851
+ projectPath: opts.projectPath,
852
+ source: "cli",
853
+ description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
854
+ });
855
+ }
856
+ }
857
+ writeFileSync(file, fixLines.join("\n"), "utf8");
858
+ }
859
+ }
860
+ return results;
861
+ }
862
+ function calculateEntropy2(str) {
863
+ if (!str) return 0;
864
+ const len = str.length;
865
+ const frequencies = /* @__PURE__ */ new Map();
866
+ for (let i = 0; i < len; i++) {
867
+ const ch = str[i];
868
+ frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
869
+ }
870
+ let entropy = 0;
871
+ for (const count of frequencies.values()) {
872
+ const p = count / len;
873
+ entropy -= p * Math.log2(p);
874
+ }
875
+ return entropy;
876
+ }
877
+
878
+ // src/core/validate.ts
879
+ function makeRequest(url, headers, timeoutMs = 1e4) {
880
+ return httpRequest_({ url, method: "GET", headers, timeoutMs });
881
+ }
882
+ var ProviderRegistry = class {
883
+ providers = /* @__PURE__ */ new Map();
884
+ register(provider) {
885
+ this.providers.set(provider.name, provider);
886
+ }
887
+ get(name) {
888
+ return this.providers.get(name);
889
+ }
890
+ detectProvider(value, hints) {
891
+ if (hints?.provider) {
892
+ return this.providers.get(hints.provider);
893
+ }
894
+ for (const provider of this.providers.values()) {
895
+ if (provider.prefixes) {
896
+ for (const pfx of provider.prefixes) {
897
+ if (value.startsWith(pfx)) return provider;
898
+ }
899
+ }
900
+ }
901
+ return void 0;
902
+ }
903
+ listProviders() {
904
+ return [...this.providers.values()];
905
+ }
906
+ };
907
+ var openaiProvider = {
908
+ name: "openai",
909
+ description: "OpenAI API key validation",
910
+ prefixes: ["sk-"],
911
+ async validate(value) {
912
+ const start = Date.now();
913
+ try {
914
+ const { statusCode } = await makeRequest(
915
+ "https://api.openai.com/v1/models?limit=1",
916
+ {
917
+ Authorization: `Bearer ${value}`,
918
+ "User-Agent": "q-ring-validator/1.0"
919
+ }
920
+ );
921
+ const latencyMs = Date.now() - start;
922
+ if (statusCode === 200)
923
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
924
+ if (statusCode === 401)
925
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
926
+ if (statusCode === 429)
927
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
928
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
929
+ } catch (err) {
930
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
931
+ }
932
+ }
933
+ };
934
+ var stripeProvider = {
935
+ name: "stripe",
936
+ description: "Stripe API key validation",
937
+ prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
938
+ async validate(value) {
939
+ const start = Date.now();
940
+ try {
941
+ const { statusCode } = await makeRequest(
942
+ "https://api.stripe.com/v1/balance",
943
+ {
944
+ Authorization: `Bearer ${value}`,
945
+ "User-Agent": "q-ring-validator/1.0"
946
+ }
947
+ );
948
+ const latencyMs = Date.now() - start;
949
+ if (statusCode === 200)
950
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
951
+ if (statusCode === 401)
952
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
953
+ if (statusCode === 429)
954
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
955
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
956
+ } catch (err) {
957
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
958
+ }
959
+ }
960
+ };
961
+ var githubProvider = {
962
+ name: "github",
963
+ description: "GitHub token validation",
964
+ prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
965
+ async validate(value) {
966
+ const start = Date.now();
967
+ try {
968
+ const { statusCode } = await makeRequest(
969
+ "https://api.github.com/user",
970
+ {
971
+ Authorization: `token ${value}`,
972
+ "User-Agent": "q-ring-validator/1.0",
973
+ Accept: "application/vnd.github+json"
974
+ }
975
+ );
976
+ const latencyMs = Date.now() - start;
977
+ if (statusCode === 200)
978
+ return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
979
+ if (statusCode === 401)
980
+ return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
981
+ if (statusCode === 403)
982
+ return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
983
+ if (statusCode === 429)
984
+ return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
985
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
986
+ } catch (err) {
987
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
988
+ }
989
+ }
990
+ };
991
+ var awsProvider = {
992
+ name: "aws",
993
+ description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
994
+ prefixes: ["AKIA", "ASIA"],
995
+ async validate(value) {
996
+ const start = Date.now();
997
+ const latencyMs = Date.now() - start;
998
+ if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
999
+ return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
1000
+ }
1001
+ return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
1002
+ }
1003
+ };
1004
+ var httpProvider = {
1005
+ name: "http",
1006
+ description: "Generic HTTP endpoint validation",
1007
+ async validate(value, url) {
1008
+ const start = Date.now();
1009
+ if (!url) {
1010
+ return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
1011
+ }
1012
+ try {
1013
+ const { statusCode } = await makeRequest(url, {
1014
+ Authorization: `Bearer ${value}`,
1015
+ "User-Agent": "q-ring-validator/1.0"
1016
+ });
1017
+ const latencyMs = Date.now() - start;
1018
+ if (statusCode >= 200 && statusCode < 300)
1019
+ return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
1020
+ if (statusCode === 401 || statusCode === 403)
1021
+ return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
1022
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
1023
+ } catch (err) {
1024
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
1025
+ }
1026
+ }
1027
+ };
1028
+ var registry2 = new ProviderRegistry();
1029
+ registry2.register(openaiProvider);
1030
+ registry2.register(stripeProvider);
1031
+ registry2.register(githubProvider);
1032
+ registry2.register(awsProvider);
1033
+ registry2.register(httpProvider);
1034
+ async function validateSecret(value, opts) {
1035
+ const provider = opts?.provider ? registry2.get(opts.provider) : registry2.detectProvider(value);
1036
+ if (!provider) {
1037
+ return {
1038
+ valid: false,
1039
+ status: "unknown",
1040
+ message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
1041
+ latencyMs: 0,
1042
+ provider: "none"
1043
+ };
1044
+ }
1045
+ if (provider.name === "http" && opts?.validationUrl) {
1046
+ return provider.validate(value, opts.validationUrl);
1047
+ }
1048
+ return provider.validate(value);
1049
+ }
1050
+ async function rotateWithProvider(value, providerName) {
1051
+ const provider = providerName ? registry2.get(providerName) : registry2.detectProvider(value);
1052
+ if (!provider) {
1053
+ return { rotated: false, provider: "none", message: "No provider detected for rotation" };
1054
+ }
1055
+ const rotatable = provider;
1056
+ if (rotatable.supportsRotation && rotatable.rotate) {
1057
+ return rotatable.rotate(value);
1058
+ }
1059
+ const format = "api-key";
1060
+ const newValue = generateSecret({ format, length: 48 });
1061
+ return {
1062
+ rotated: true,
1063
+ provider: provider.name,
1064
+ message: `Provider "${provider.name}" does not support native rotation \u2014 generated new value locally`,
1065
+ newValue
1066
+ };
1067
+ }
1068
+ async function ciValidateBatch(secrets) {
1069
+ const results = [];
1070
+ for (const s of secrets) {
1071
+ const validation = await validateSecret(s.value, {
1072
+ provider: s.provider,
1073
+ validationUrl: s.validationUrl
1074
+ });
1075
+ results.push({
1076
+ key: s.key,
1077
+ validation,
1078
+ requiresRotation: validation.status === "invalid"
1079
+ });
1080
+ }
1081
+ const failCount = results.filter((r) => !r.validation.valid).length;
1082
+ return { results, allValid: failCount === 0, failCount };
1083
+ }
1084
+
1085
+ // src/core/context.ts
1086
+ function getProjectContext(opts = {}) {
1087
+ const projectPath = opts.projectPath ?? process.cwd();
1088
+ const envResult = collapseEnvironment({ projectPath });
1089
+ const secretsList = listSecrets({
1090
+ ...opts,
1091
+ projectPath,
1092
+ silent: true
1093
+ });
1094
+ let expiredCount = 0;
1095
+ let staleCount = 0;
1096
+ let protectedCount = 0;
1097
+ const secrets = secretsList.map((entry) => {
1098
+ const meta = entry.envelope?.meta;
1099
+ const decay = entry.decay;
1100
+ if (decay?.isExpired) expiredCount++;
1101
+ if (decay?.isStale) staleCount++;
1102
+ if (meta?.requiresApproval) protectedCount++;
1103
+ return {
1104
+ key: entry.key,
1105
+ scope: entry.scope,
1106
+ tags: meta?.tags,
1107
+ description: meta?.description,
1108
+ provider: meta?.provider,
1109
+ requiresApproval: meta?.requiresApproval,
1110
+ jitProvider: meta?.jitProvider,
1111
+ hasStates: !!(entry.envelope?.states && Object.keys(entry.envelope.states).length > 0),
1112
+ isExpired: decay?.isExpired ?? false,
1113
+ isStale: decay?.isStale ?? false,
1114
+ timeRemaining: decay?.timeRemaining ?? null,
1115
+ accessCount: meta?.accessCount ?? 0,
1116
+ lastAccessed: meta?.lastAccessedAt ?? null,
1117
+ rotationFormat: meta?.rotationFormat
1118
+ };
1119
+ });
1120
+ let manifest = null;
1121
+ const config = readProjectConfig(projectPath);
1122
+ if (config?.secrets) {
1123
+ const declaredKeys = Object.keys(config.secrets);
1124
+ const existingKeys = new Set(secrets.map((s) => s.key));
1125
+ const missing = declaredKeys.filter((k) => !existingKeys.has(k));
1126
+ manifest = { declared: declaredKeys.length, missing };
1127
+ }
1128
+ const recentEvents = queryAudit({ limit: 20 });
1129
+ const recentActions = recentEvents.map((e) => ({
1130
+ action: e.action,
1131
+ key: e.key,
1132
+ source: e.source,
1133
+ timestamp: e.timestamp
1134
+ }));
1135
+ return {
1136
+ projectPath,
1137
+ environment: envResult ? { env: envResult.env, source: envResult.source } : null,
1138
+ secrets,
1139
+ totalSecrets: secrets.length,
1140
+ expiredCount,
1141
+ staleCount,
1142
+ protectedCount,
1143
+ manifest,
1144
+ validationProviders: registry2.listProviders().map((p) => p.name),
1145
+ jitProviders: registry.listProviders().map((p) => p.name),
1146
+ hooksCount: listHooks().length,
1147
+ recentActions
1148
+ };
1149
+ }
1150
+
1151
+ // src/core/memory.ts
1152
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
1153
+ import { join as join2 } from "path";
1154
+ import { homedir, hostname, userInfo } from "os";
1155
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
1156
+ var MEMORY_FILE = "agent-memory.enc";
1157
+ function getMemoryDir() {
1158
+ const dir = join2(homedir(), ".config", "q-ring");
1159
+ if (!existsSync2(dir)) {
1160
+ mkdirSync(dir, { recursive: true });
1161
+ }
1162
+ return dir;
1163
+ }
1164
+ function getMemoryPath() {
1165
+ return join2(getMemoryDir(), MEMORY_FILE);
1166
+ }
1167
+ function deriveKey2() {
1168
+ const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1169
+ return createHash("sha256").update(fingerprint).digest();
1170
+ }
1171
+ function encrypt(data) {
1172
+ const key = deriveKey2();
1173
+ const iv = randomBytes3(12);
1174
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
1175
+ const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
1176
+ const tag = cipher.getAuthTag();
1177
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
1178
+ }
1179
+ function decrypt(blob) {
1180
+ const parts = blob.split(":");
1181
+ if (parts.length !== 3) throw new Error("Invalid encrypted format");
1182
+ const iv = Buffer.from(parts[0], "base64");
1183
+ const tag = Buffer.from(parts[1], "base64");
1184
+ const encrypted = Buffer.from(parts[2], "base64");
1185
+ const key = deriveKey2();
1186
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
1187
+ decipher.setAuthTag(tag);
1188
+ return decipher.update(encrypted) + decipher.final("utf8");
1189
+ }
1190
+ function loadStore() {
1191
+ const path = getMemoryPath();
1192
+ if (!existsSync2(path)) {
1193
+ return { entries: {} };
1194
+ }
1195
+ try {
1196
+ const raw = readFileSync4(path, "utf8");
1197
+ const decrypted = decrypt(raw);
1198
+ return JSON.parse(decrypted);
1199
+ } catch {
1200
+ return { entries: {} };
1201
+ }
1202
+ }
1203
+ function saveStore(store) {
1204
+ const json = JSON.stringify(store);
1205
+ const encrypted = encrypt(json);
1206
+ writeFileSync2(getMemoryPath(), encrypted, "utf8");
1207
+ }
1208
+ function remember(key, value) {
1209
+ const store = loadStore();
1210
+ store.entries[key] = {
1211
+ value,
1212
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1213
+ };
1214
+ saveStore(store);
1215
+ }
1216
+ function recall(key) {
1217
+ const store = loadStore();
1218
+ return store.entries[key]?.value ?? null;
1219
+ }
1220
+ function listMemory() {
1221
+ const store = loadStore();
1222
+ return Object.entries(store.entries).map(([key, entry]) => ({
1223
+ key,
1224
+ updatedAt: entry.updatedAt
1225
+ }));
1226
+ }
1227
+ function forget(key) {
1228
+ const store = loadStore();
1229
+ if (key in store.entries) {
1230
+ delete store.entries[key];
1231
+ saveStore(store);
1232
+ return true;
1233
+ }
1234
+ return false;
1235
+ }
1236
+ function clearMemory() {
1237
+ saveStore({ entries: {} });
1238
+ }
1239
+
1240
+ // src/hooks/precommit.ts
1241
+ import { execSync } from "child_process";
1242
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
1243
+ import { join as join3 } from "path";
1244
+ function getStagedFiles() {
1245
+ try {
1246
+ const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
1247
+ encoding: "utf8",
1248
+ stdio: ["pipe", "pipe", "pipe"]
1249
+ });
1250
+ return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
1251
+ } catch {
1252
+ return [];
1253
+ }
1254
+ }
1255
+ function runPreCommitScan() {
1256
+ const staged = getStagedFiles();
1257
+ if (staged.length === 0) return 0;
1258
+ const results = lintFiles(staged);
1259
+ if (results.length === 0) return 0;
1260
+ console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
1261
+ for (const r of results) {
1262
+ console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
1263
+ }
1264
+ console.error(
1265
+ `
1266
+ [q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
1267
+ `
1268
+ );
1269
+ return 1;
1270
+ }
1271
+ var HOOK_SCRIPT = `#!/bin/sh
1272
+ # q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
1273
+ npx qring hook:run 2>&1
1274
+ exit $?
1275
+ `;
1276
+ function installPreCommitHook(repoPath) {
1277
+ const root = repoPath ?? process.cwd();
1278
+ const hooksDir = join3(root, ".git", "hooks");
1279
+ if (!existsSync3(join3(root, ".git"))) {
1280
+ return { installed: false, path: "", message: "Not a git repository" };
1281
+ }
1282
+ const hookPath = join3(hooksDir, "pre-commit");
1283
+ if (existsSync3(hookPath)) {
1284
+ const existing = readFileSync5(hookPath, "utf8");
1285
+ if (existing.includes("q-ring")) {
1286
+ return { installed: true, path: hookPath, message: "Hook already installed" };
1287
+ }
1288
+ writeFileSync3(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
1289
+ } else {
1290
+ writeFileSync3(hookPath, HOOK_SCRIPT, "utf8");
1291
+ }
1292
+ chmodSync(hookPath, 493);
1293
+ return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
1294
+ }
1295
+ function uninstallPreCommitHook(repoPath) {
1296
+ const root = repoPath ?? process.cwd();
1297
+ const hookPath = join3(root, ".git", "hooks", "pre-commit");
1298
+ if (!existsSync3(hookPath)) return false;
1299
+ const content = readFileSync5(hookPath, "utf8");
1300
+ if (!content.includes("q-ring")) return false;
1301
+ const lines = content.split("\n");
1302
+ const cleaned = lines.filter(
1303
+ (l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
1304
+ );
1305
+ if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
1306
+ unlinkSync(hookPath);
1307
+ } else {
1308
+ writeFileSync3(hookPath, cleaned.join("\n"), "utf8");
1309
+ }
1310
+ return true;
1311
+ }
1312
+
1313
+ // src/cli/commands.ts
1314
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1315
+
356
1316
  // src/utils/prompt.ts
357
1317
  import { createInterface } from "readline";
358
1318
  async function promptSecret(message) {
@@ -405,6 +1365,8 @@ function buildOpts(cmd) {
405
1365
  let scope;
406
1366
  if (cmd.global) scope = "global";
407
1367
  else if (cmd.project) scope = "project";
1368
+ else if (cmd.team) scope = "team";
1369
+ else if (cmd.org) scope = "org";
408
1370
  const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
409
1371
  if (scope === "project" && !projectPath) {
410
1372
  throw new Error("Project path is required for project scope");
@@ -412,6 +1374,8 @@ function buildOpts(cmd) {
412
1374
  return {
413
1375
  scope,
414
1376
  projectPath: projectPath ?? process.cwd(),
1377
+ teamId: cmd.team,
1378
+ orgId: cmd.org,
415
1379
  env: cmd.env,
416
1380
  source: "cli"
417
1381
  };
@@ -419,8 +1383,8 @@ function buildOpts(cmd) {
419
1383
  function createProgram() {
420
1384
  const program2 = new Command().name("qring").description(
421
1385
  `${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
422
- ).version("0.2.0");
423
- 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) => {
1386
+ ).version("0.4.0");
1387
+ 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("--team <id>", "Store in team scope").option("--org <id>", "Store in org scope").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-)").option("--requires-approval", "Require explicit user approval for MCP agents to read").option("--jit-provider <provider>", "Use a Just-In-Time provider to dynamically generate this secret").action(async (key, value, cmd) => {
424
1388
  const opts = buildOpts(cmd);
425
1389
  if (!value) {
426
1390
  value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
@@ -434,7 +1398,11 @@ function createProgram() {
434
1398
  ttlSeconds: cmd.ttl,
435
1399
  expiresAt: cmd.expires,
436
1400
  description: cmd.description,
437
- tags: cmd.tags?.split(",").map((t) => t.trim())
1401
+ tags: cmd.tags?.split(",").map((t) => t.trim()),
1402
+ rotationFormat: cmd.rotationFormat,
1403
+ rotationPrefix: cmd.rotationPrefix,
1404
+ requiresApproval: cmd.requiresApproval,
1405
+ jitProvider: cmd.jitProvider
438
1406
  };
439
1407
  if (cmd.env) {
440
1408
  const existing = getEnvelope(key, opts);
@@ -459,7 +1427,7 @@ function createProgram() {
459
1427
  );
460
1428
  }
461
1429
  });
462
- program2.command("get <key>").description("Retrieve a secret (collapses superposition if needed)").option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force a specific environment").action((key, cmd) => {
1430
+ program2.command("get <key>").description("Retrieve a secret (collapses superposition if needed)").option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--team <id>", "Look only in team scope").option("--org <id>", "Look only in org scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force a specific environment").action((key, cmd) => {
463
1431
  const opts = buildOpts(cmd);
464
1432
  const value = getSecret(key, opts);
465
1433
  if (value === null) {
@@ -478,9 +1446,29 @@ function createProgram() {
478
1446
  process.exit(1);
479
1447
  }
480
1448
  });
481
- 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) => {
1449
+ 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("--team <id>", "List team scope only").option("--org <id>", "List org 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) => {
482
1450
  const opts = buildOpts(cmd);
483
- const entries = listSecrets(opts);
1451
+ let entries = listSecrets(opts);
1452
+ if (cmd.tag) {
1453
+ entries = entries.filter(
1454
+ (e) => e.envelope?.meta.tags?.includes(cmd.tag)
1455
+ );
1456
+ }
1457
+ if (cmd.expired) {
1458
+ entries = entries.filter((e) => e.decay?.isExpired);
1459
+ }
1460
+ if (cmd.stale) {
1461
+ entries = entries.filter(
1462
+ (e) => e.decay?.isStale && !e.decay?.isExpired
1463
+ );
1464
+ }
1465
+ if (cmd.filter) {
1466
+ const regex = new RegExp(
1467
+ "^" + cmd.filter.replace(/\*/g, ".*") + "$",
1468
+ "i"
1469
+ );
1470
+ entries = entries.filter((e) => regex.test(e.key));
1471
+ }
484
1472
  if (entries.length === 0) {
485
1473
  console.log(c.dim("No secrets found"));
486
1474
  return;
@@ -593,50 +1581,605 @@ function createProgram() {
593
1581
  }
594
1582
  console.log();
595
1583
  });
596
- 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) => {
1584
+ 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) => {
597
1585
  const opts = buildOpts(cmd);
598
- const output = exportSecrets({ ...opts, format: cmd.format });
1586
+ const output = exportSecrets({
1587
+ ...opts,
1588
+ format: cmd.format,
1589
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
1590
+ tags: cmd.tags?.split(",").map((t) => t.trim())
1591
+ });
599
1592
  process.stdout.write(output + "\n");
600
1593
  });
601
- program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
602
- const result = collapseEnvironment({
603
- projectPath: cmd.projectPath ?? process.cwd()
1594
+ 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) => {
1595
+ const opts = buildOpts(cmd);
1596
+ const result = importDotenv(file, {
1597
+ scope: opts.scope,
1598
+ projectPath: opts.projectPath,
1599
+ source: "cli",
1600
+ skipExisting: cmd.skipExisting,
1601
+ dryRun: cmd.dryRun
604
1602
  });
605
- if (result) {
1603
+ if (cmd.dryRun) {
606
1604
  console.log(
607
- `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
1605
+ `
1606
+ ${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
1607
+ `
608
1608
  );
1609
+ for (const key of result.imported) {
1610
+ console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
1611
+ }
1612
+ if (result.skipped.length > 0) {
1613
+ console.log(`
1614
+ ${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
1615
+ }
609
1616
  } else {
610
1617
  console.log(
611
- c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
1618
+ `${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
612
1619
  );
1620
+ if (result.skipped.length > 0) {
1621
+ console.log(
1622
+ c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
1623
+ );
1624
+ }
613
1625
  }
1626
+ console.log();
614
1627
  });
615
- program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
616
- "-f, --format <format>",
617
- "Format: hex, base64, alphanumeric, uuid, api-key, token, password",
618
- "api-key"
619
- ).option("-l, --length <n>", "Length (bytes or chars depending on format)", parseInt).option("--prefix <prefix>", "Prefix for api-key/token format").option("-s, --save <key>", "Save the generated secret to keyring with this key").option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
620
- const secret = generateSecret({
621
- format: cmd.format,
622
- length: cmd.length,
623
- prefix: cmd.prefix
624
- });
625
- const entropy = estimateEntropy(secret);
626
- if (cmd.save) {
627
- const opts = buildOpts(cmd);
628
- setSecret(cmd.save, secret, opts);
1628
+ program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
1629
+ const projectPath = cmd.projectPath ?? process.cwd();
1630
+ const config = readProjectConfig(projectPath);
1631
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1632
+ console.error(
1633
+ c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
1634
+ );
629
1635
  console.log(
630
- `${SYMBOLS.sparkle} ${c.green("generated & saved")} ${c.bold(cmd.save)} ${c.dim(`(${cmd.format}, ${entropy} bits entropy)`)}`
1636
+ c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
631
1637
  );
632
- } else {
633
- process.stdout.write(secret);
634
- if (process.stdout.isTTY) {
1638
+ process.exit(1);
1639
+ }
1640
+ console.log(
1641
+ c.bold(`
1642
+ ${SYMBOLS.shield} Project secret manifest check
1643
+ `)
1644
+ );
1645
+ let present = 0;
1646
+ let missing = 0;
1647
+ let expiredCount = 0;
1648
+ let staleCount = 0;
1649
+ for (const [key, manifest] of Object.entries(config.secrets)) {
1650
+ const result = getEnvelope(key, { projectPath, source: "cli" });
1651
+ if (!result) {
1652
+ if (manifest.required !== false) {
1653
+ missing++;
1654
+ console.log(
1655
+ ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1656
+ );
1657
+ } else {
1658
+ console.log(
1659
+ ` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1660
+ );
1661
+ }
1662
+ continue;
1663
+ }
1664
+ const decay = checkDecay(result.envelope);
1665
+ if (decay.isExpired) {
1666
+ expiredCount++;
635
1667
  console.log(
636
- `
637
- ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
1668
+ ` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
638
1669
  );
639
- }
1670
+ } else if (decay.isStale) {
1671
+ staleCount++;
1672
+ console.log(
1673
+ ` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1674
+ );
1675
+ } else {
1676
+ present++;
1677
+ console.log(
1678
+ ` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1679
+ );
1680
+ }
1681
+ }
1682
+ const total = Object.keys(config.secrets).length;
1683
+ console.log(
1684
+ `
1685
+ ${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
1686
+ );
1687
+ if (missing > 0) {
1688
+ console.log(
1689
+ `
1690
+ ${c.red("Project is NOT ready \u2014 missing required secrets.")}`
1691
+ );
1692
+ } else if (expiredCount > 0) {
1693
+ console.log(
1694
+ `
1695
+ ${c.yellow("Project has expired secrets that need rotation.")}`
1696
+ );
1697
+ } else {
1698
+ console.log(
1699
+ `
1700
+ ${c.green(`${SYMBOLS.check} Project is ready \u2014 all required secrets present.`)}`
1701
+ );
1702
+ }
1703
+ console.log();
1704
+ if (missing > 0) process.exit(1);
1705
+ });
1706
+ 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) => {
1707
+ if (cmd.listProviders) {
1708
+ console.log(c.bold(`
1709
+ ${SYMBOLS.shield} Available validation providers
1710
+ `));
1711
+ for (const p of registry2.listProviders()) {
1712
+ const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
1713
+ console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
1714
+ }
1715
+ console.log();
1716
+ return;
1717
+ }
1718
+ if (!key && !cmd.all) {
1719
+ console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
1720
+ process.exit(1);
1721
+ }
1722
+ const opts = buildOpts(cmd);
1723
+ if (cmd.all) {
1724
+ let entries = listSecrets(opts);
1725
+ const projectPath = cmd.projectPath ?? process.cwd();
1726
+ if (cmd.manifest) {
1727
+ const config = readProjectConfig(projectPath);
1728
+ if (config?.secrets) {
1729
+ const manifestKeys = new Set(Object.keys(config.secrets));
1730
+ entries = entries.filter((e) => manifestKeys.has(e.key));
1731
+ }
1732
+ }
1733
+ console.log(c.bold(`
1734
+ ${SYMBOLS.shield} Validating secrets
1735
+ `));
1736
+ let validated = 0;
1737
+ let skipped = 0;
1738
+ for (const entry of entries) {
1739
+ const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
1740
+ if (!value2) {
1741
+ skipped++;
1742
+ continue;
1743
+ }
1744
+ const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
1745
+ const result2 = await validateSecret(value2, { provider: provHint2 });
1746
+ if (result2.status === "unknown") {
1747
+ skipped++;
1748
+ continue;
1749
+ }
1750
+ validated++;
1751
+ const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
1752
+ const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
1753
+ console.log(
1754
+ ` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
1755
+ );
1756
+ }
1757
+ console.log(`
1758
+ ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
1759
+ `);
1760
+ return;
1761
+ }
1762
+ const value = getSecret(key, opts);
1763
+ if (!value) {
1764
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1765
+ process.exit(1);
1766
+ }
1767
+ const envelope = getEnvelope(key, opts);
1768
+ const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
1769
+ const result = await validateSecret(value, { provider: provHint });
1770
+ 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");
1771
+ console.log(`
1772
+ ${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
1773
+ if (result.message && result.status !== "valid") {
1774
+ console.log(` ${c.dim(result.message)}`);
1775
+ }
1776
+ console.log();
1777
+ });
1778
+ program2.command("exec <command...>").description("Run a command with secrets injected into its environment (output auto-redacted)").option("-g, --global", "Inject global scope secrets only").option("-p, --project", "Inject project scope secrets only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("-k, --keys <keys>", "Comma-separated key names to inject").option("-t, --tags <tags>", "Comma-separated tags to filter by").option("--profile <name>", "Exec profile: unrestricted, restricted, ci").action(async (commandArgs, cmd) => {
1779
+ const opts = buildOpts(cmd);
1780
+ const command = commandArgs[0];
1781
+ const args = commandArgs.slice(1);
1782
+ try {
1783
+ const { code } = await execCommand({
1784
+ ...opts,
1785
+ command,
1786
+ args,
1787
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
1788
+ tags: cmd.tags?.split(",").map((t) => t.trim()),
1789
+ profile: cmd.profile
1790
+ });
1791
+ process.exit(code);
1792
+ } catch (err) {
1793
+ console.error(c.red(`${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`));
1794
+ process.exit(1);
1795
+ }
1796
+ });
1797
+ program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option("--fix", "Auto-replace hardcoded secrets with process.env references and store in q-ring").option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((dir, cmd) => {
1798
+ const targetDir = dir ?? process.cwd();
1799
+ const fixMode = cmd.fix === true;
1800
+ console.log(`
1801
+ ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
1802
+ `);
1803
+ const results = scanCodebase(targetDir);
1804
+ if (results.length === 0) {
1805
+ console.log(` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
1806
+ `);
1807
+ return;
1808
+ }
1809
+ if (fixMode) {
1810
+ const fileSet = new Set(results.map((r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`));
1811
+ const opts = buildOpts(cmd);
1812
+ const lintResults = lintFiles([...fileSet], { fix: true, scope: opts.scope, projectPath: opts.projectPath });
1813
+ const fixedCount = lintResults.filter((r) => r.fixed).length;
1814
+ console.log(` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
1815
+ `);
1816
+ return;
1817
+ }
1818
+ for (const res of results) {
1819
+ console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
1820
+ console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
1821
+ console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
1822
+ console.log(` ${c.dim("Context:")} ${res.context}`);
1823
+ console.log();
1824
+ }
1825
+ console.log(` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
1826
+ `);
1827
+ });
1828
+ program2.command("lint <files...>").description("Lint specific files for hardcoded secrets (with optional auto-fix)").option("--fix", "Replace hardcoded secrets with process.env references and store in q-ring").option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((files, cmd) => {
1829
+ const opts = buildOpts(cmd);
1830
+ const results = lintFiles(files, { fix: cmd.fix, scope: opts.scope, projectPath: opts.projectPath });
1831
+ if (results.length === 0) {
1832
+ console.log(`
1833
+ ${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
1834
+ `);
1835
+ return;
1836
+ }
1837
+ for (const res of results) {
1838
+ const status = res.fixed ? c.green("fixed") : c.red("found");
1839
+ console.log(` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`);
1840
+ console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
1841
+ console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
1842
+ console.log();
1843
+ }
1844
+ const fixedCount = results.filter((r) => r.fixed).length;
1845
+ if (cmd.fix && fixedCount > 0) {
1846
+ console.log(` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
1847
+ `);
1848
+ } else {
1849
+ console.log(` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
1850
+ `);
1851
+ }
1852
+ });
1853
+ program2.command("context").alias("describe").description("Show safe, redacted project context for AI agents (no secret values exposed)").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON (for MCP / programmatic use)").action((cmd) => {
1854
+ const opts = buildOpts(cmd);
1855
+ const context = getProjectContext(opts);
1856
+ if (cmd.json) {
1857
+ console.log(JSON.stringify(context, null, 2));
1858
+ return;
1859
+ }
1860
+ console.log(`
1861
+ ${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
1862
+ console.log(` Project: ${c.cyan(context.projectPath)}`);
1863
+ if (context.environment) {
1864
+ console.log(` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`);
1865
+ }
1866
+ console.log(`
1867
+ ${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`);
1868
+ for (const s of context.secrets) {
1869
+ const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
1870
+ const flags = [];
1871
+ if (s.requiresApproval) flags.push(c.yellow("locked"));
1872
+ if (s.jitProvider) flags.push(c.magenta("jit"));
1873
+ if (s.hasStates) flags.push(c.blue("superposition"));
1874
+ if (s.isExpired) flags.push(c.red("expired"));
1875
+ else if (s.isStale) flags.push(c.yellow("stale"));
1876
+ const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
1877
+ console.log(` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`);
1878
+ }
1879
+ if (context.manifest) {
1880
+ console.log(`
1881
+ ${c.bold(" Manifest:")} ${context.manifest.declared} declared`);
1882
+ if (context.manifest.missing.length > 0) {
1883
+ console.log(` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`);
1884
+ } else {
1885
+ console.log(` ${c.green(SYMBOLS.check)} All manifest secrets present`);
1886
+ }
1887
+ }
1888
+ console.log(`
1889
+ ${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`);
1890
+ console.log(`${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`);
1891
+ console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
1892
+ if (context.recentActions.length > 0) {
1893
+ console.log(`
1894
+ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`);
1895
+ for (const a of context.recentActions.slice(0, 8)) {
1896
+ const ts = new Date(a.timestamp).toLocaleTimeString();
1897
+ console.log(` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`);
1898
+ }
1899
+ }
1900
+ console.log();
1901
+ });
1902
+ program2.command("remember <key> <value>").description("Store a key-value pair in encrypted agent memory (persists across sessions)").action((key, value) => {
1903
+ remember(key, value);
1904
+ console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
1905
+ });
1906
+ program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
1907
+ if (!key) {
1908
+ const entries = listMemory();
1909
+ if (entries.length === 0) {
1910
+ console.log(c.dim("Agent memory is empty."));
1911
+ return;
1912
+ }
1913
+ console.log(`
1914
+ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
1915
+ `);
1916
+ for (const e of entries) {
1917
+ console.log(` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`);
1918
+ }
1919
+ console.log();
1920
+ return;
1921
+ }
1922
+ const value = recall(key);
1923
+ if (value === null) {
1924
+ console.log(c.dim(`No memory found for "${key}"`));
1925
+ } else {
1926
+ console.log(safeStr(value));
1927
+ }
1928
+ });
1929
+ program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
1930
+ if (cmd.all) {
1931
+ clearMemory();
1932
+ console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
1933
+ return;
1934
+ }
1935
+ const removed = forget(key);
1936
+ if (removed) {
1937
+ console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
1938
+ } else {
1939
+ console.log(c.dim(`No memory found for "${key}"`));
1940
+ }
1941
+ });
1942
+ program2.command("approve <key>").description("Grant a scoped, reasoned, HMAC-verified approval token for MCP secret access").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--for <seconds>", "Duration of approval in seconds", parseInt, 3600).option("--reason <text>", "Reason for granting approval").option("--revoke", "Revoke an existing approval").option("--list", "List all approvals").action((key, cmd) => {
1943
+ const opts = buildOpts(cmd);
1944
+ const scope = opts.scope ?? "global";
1945
+ if (cmd.list) {
1946
+ const approvals = listApprovals();
1947
+ if (approvals.length === 0) {
1948
+ console.log(c.dim(" No active approvals"));
1949
+ return;
1950
+ }
1951
+ for (const a of approvals) {
1952
+ const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
1953
+ const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
1954
+ console.log(` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`);
1955
+ }
1956
+ return;
1957
+ }
1958
+ if (cmd.revoke) {
1959
+ const revoked = revokeApproval(key, scope);
1960
+ if (revoked) {
1961
+ console.log(`${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`);
1962
+ } else {
1963
+ console.log(c.dim(` No active approval found for ${key}`));
1964
+ }
1965
+ return;
1966
+ }
1967
+ const entry = grantApproval(key, scope, cmd.for, {
1968
+ reason: cmd.reason ?? "manual approval"
1969
+ });
1970
+ console.log(`${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`);
1971
+ console.log(c.dim(` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`));
1972
+ });
1973
+ program2.command("approvals").description("List all approval tokens with verification status").action(() => {
1974
+ const approvals = listApprovals();
1975
+ if (approvals.length === 0) {
1976
+ console.log(c.dim(" No approvals found"));
1977
+ return;
1978
+ }
1979
+ console.log(c.bold("\n\u{1F510} Approval Tokens\n"));
1980
+ for (const a of approvals) {
1981
+ const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
1982
+ const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
1983
+ console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
1984
+ console.log(c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`));
1985
+ if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
1986
+ }
1987
+ console.log();
1988
+ });
1989
+ program2.command("hook:install").description("Install a git pre-commit hook that scans for hardcoded secrets").option("--project-path <path>", "Repository path").action((cmd) => {
1990
+ const result = installPreCommitHook(cmd.projectPath);
1991
+ if (result.installed) {
1992
+ console.log(`${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`);
1993
+ } else {
1994
+ console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
1995
+ }
1996
+ });
1997
+ program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
1998
+ const removed = uninstallPreCommitHook(cmd.projectPath);
1999
+ if (removed) {
2000
+ console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
2001
+ } else {
2002
+ console.log(c.dim("No q-ring pre-commit hook found"));
2003
+ }
2004
+ });
2005
+ program2.command("hook:run").description("Run the pre-commit secret scan (called by the git hook)").action(() => {
2006
+ const code = runPreCommitScan();
2007
+ process.exit(code);
2008
+ });
2009
+ program2.command("wizard <name>").description("Set up a new service integration with secrets, manifest, and hooks").option("--keys <keys>", "Comma-separated secret key names to create (e.g. API_KEY,API_SECRET)").option("--provider <provider>", "Validation provider (e.g. openai, stripe, github)").option("--tags <tags>", "Comma-separated tags for all secrets").option("--hook-exec <cmd>", "Shell command to run when any of these secrets change").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").action(async (name, cmd) => {
2010
+ const opts = buildOpts(cmd);
2011
+ const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2012
+ const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [name.toLowerCase()];
2013
+ const provider = cmd.provider;
2014
+ let keyNames;
2015
+ if (cmd.keys) {
2016
+ keyNames = cmd.keys.split(",").map((k) => k.trim());
2017
+ } else {
2018
+ keyNames = [`${prefix}_API_KEY`, `${prefix}_API_SECRET`];
2019
+ }
2020
+ console.log(`
2021
+ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
2022
+ `);
2023
+ for (const key of keyNames) {
2024
+ const value = generateSecret({ format: "api-key", prefix: `${prefix.toLowerCase()}_` });
2025
+ setSecret(key, value, {
2026
+ ...opts,
2027
+ tags,
2028
+ provider,
2029
+ description: `Auto-generated by wizard for ${name}`
2030
+ });
2031
+ console.log(` ${c.green(SYMBOLS.check)} Created ${c.bold(key)}`);
2032
+ }
2033
+ const projectPath = opts.projectPath ?? process.cwd();
2034
+ const manifestPath = `${projectPath}/.q-ring.json`;
2035
+ let config = {};
2036
+ try {
2037
+ if (existsSync4(manifestPath)) {
2038
+ config = JSON.parse(readFileSync6(manifestPath, "utf8"));
2039
+ }
2040
+ } catch {
2041
+ }
2042
+ if (!config.secrets) config.secrets = {};
2043
+ for (const key of keyNames) {
2044
+ config.secrets[key] = {
2045
+ required: true,
2046
+ description: `${name} integration`,
2047
+ ...provider ? { provider } : {}
2048
+ };
2049
+ }
2050
+ writeFileSync4(manifestPath, JSON.stringify(config, null, 2) + "\n", "utf8");
2051
+ console.log(` ${c.green(SYMBOLS.check)} Updated ${c.dim(".q-ring.json")} manifest`);
2052
+ if (cmd.hookExec) {
2053
+ for (const key of keyNames) {
2054
+ registerHook({
2055
+ type: "shell",
2056
+ match: { key, action: ["write", "delete"] },
2057
+ command: cmd.hookExec,
2058
+ description: `${name} wizard hook`,
2059
+ enabled: true
2060
+ });
2061
+ }
2062
+ console.log(` ${c.green(SYMBOLS.check)} Registered hook: ${c.dim(cmd.hookExec)}`);
2063
+ }
2064
+ console.log(`
2065
+ ${c.green("Done!")} Service "${name}" is ready with ${keyNames.length} secrets.
2066
+ `);
2067
+ });
2068
+ program2.command("analyze").description("Analyze secret usage patterns and provide optimization suggestions").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
2069
+ const opts = buildOpts(cmd);
2070
+ const entries = listSecrets({ ...opts, silent: true });
2071
+ const audit = queryAudit({ limit: 1e3 });
2072
+ console.log(`
2073
+ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2074
+ `);
2075
+ const accessMap = /* @__PURE__ */ new Map();
2076
+ for (const e of audit) {
2077
+ if (e.action === "read" && e.key) {
2078
+ accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
2079
+ }
2080
+ }
2081
+ const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
2082
+ if (sorted.length > 0) {
2083
+ console.log(` ${c.bold("Most accessed:")}`);
2084
+ for (const [key, count] of sorted.slice(0, 5)) {
2085
+ console.log(` ${c.bold(key)} \u2014 ${c.cyan(count.toString())} reads`);
2086
+ }
2087
+ console.log();
2088
+ }
2089
+ const neverAccessed = entries.filter((e) => {
2090
+ const count = e.envelope?.meta.accessCount ?? 0;
2091
+ return count === 0;
2092
+ });
2093
+ if (neverAccessed.length > 0) {
2094
+ console.log(` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`);
2095
+ for (const e of neverAccessed.slice(0, 8)) {
2096
+ const age = e.envelope?.meta.createdAt ? c.dim(`(created ${new Date(e.envelope.meta.createdAt).toLocaleDateString()})`) : "";
2097
+ console.log(` ${c.dim(SYMBOLS.cross)} ${e.key} ${age}`);
2098
+ }
2099
+ console.log();
2100
+ }
2101
+ const expired = entries.filter((e) => e.decay?.isExpired);
2102
+ const stale = entries.filter((e) => e.decay?.isStale && !e.decay?.isExpired);
2103
+ if (expired.length > 0) {
2104
+ console.log(` ${c.red("Expired:")} ${expired.length} secrets need rotation or cleanup`);
2105
+ for (const e of expired.slice(0, 5)) {
2106
+ console.log(` ${c.red(SYMBOLS.cross)} ${e.key}`);
2107
+ }
2108
+ console.log();
2109
+ }
2110
+ if (stale.length > 0) {
2111
+ console.log(` ${c.yellow("Stale (>75% lifetime):")} ${stale.length} secrets approaching expiry`);
2112
+ for (const e of stale.slice(0, 5)) {
2113
+ console.log(` ${c.yellow(SYMBOLS.warning)} ${e.key} ${c.dim(`(${e.decay?.timeRemaining} remaining)`)}`);
2114
+ }
2115
+ console.log();
2116
+ }
2117
+ const globalOnly = entries.filter((e) => e.scope === "global");
2118
+ const withProjectTags = globalOnly.filter(
2119
+ (e) => e.envelope?.meta.tags?.some((t) => ["backend", "frontend", "db", "api"].includes(t))
2120
+ );
2121
+ if (withProjectTags.length > 0) {
2122
+ console.log(` ${c.bold("Scope suggestions:")}`);
2123
+ console.log(` ${withProjectTags.length} global secret(s) have project-specific tags \u2014 consider moving to project scope`);
2124
+ console.log();
2125
+ }
2126
+ const noRotation = entries.filter(
2127
+ (e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
2128
+ );
2129
+ if (noRotation.length > 0) {
2130
+ console.log(` ${c.bold("Rotation suggestions:")}`);
2131
+ console.log(` ${noRotation.length} secret(s) have no rotation format set`);
2132
+ console.log(` Use ${c.bold("qring set <key> <value> --rotation-format api-key")} to enable auto-rotation`);
2133
+ console.log();
2134
+ }
2135
+ console.log(` ${c.bold("Summary:")}`);
2136
+ console.log(` Total secrets: ${entries.length}`);
2137
+ console.log(` Active: ${entries.length - expired.length}`);
2138
+ console.log(` Expired: ${expired.length}`);
2139
+ console.log(` Stale: ${stale.length}`);
2140
+ console.log(` Never accessed: ${neverAccessed.length}`);
2141
+ console.log(` With rotation config: ${entries.length - noRotation.length}`);
2142
+ console.log();
2143
+ });
2144
+ program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
2145
+ const result = collapseEnvironment({
2146
+ projectPath: cmd.projectPath ?? process.cwd()
2147
+ });
2148
+ if (result) {
2149
+ console.log(
2150
+ `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
2151
+ );
2152
+ } else {
2153
+ console.log(
2154
+ c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
2155
+ );
2156
+ }
2157
+ });
2158
+ program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
2159
+ "-f, --format <format>",
2160
+ "Format: hex, base64, alphanumeric, uuid, api-key, token, password",
2161
+ "api-key"
2162
+ ).option("-l, --length <n>", "Length (bytes or chars depending on format)", parseInt).option("--prefix <prefix>", "Prefix for api-key/token format").option("-s, --save <key>", "Save the generated secret to keyring with this key").option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
2163
+ const secret = generateSecret({
2164
+ format: cmd.format,
2165
+ length: cmd.length,
2166
+ prefix: cmd.prefix
2167
+ });
2168
+ const entropy = estimateEntropy(secret);
2169
+ if (cmd.save) {
2170
+ const opts = buildOpts(cmd);
2171
+ setSecret(cmd.save, secret, opts);
2172
+ console.log(
2173
+ `${SYMBOLS.sparkle} ${c.green("generated & saved")} ${c.bold(cmd.save)} ${c.dim(`(${cmd.format}, ${entropy} bits entropy)`)}`
2174
+ );
2175
+ } else {
2176
+ process.stdout.write(secret);
2177
+ if (process.stdout.isTTY) {
2178
+ console.log(
2179
+ `
2180
+ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
2181
+ );
2182
+ }
640
2183
  }
641
2184
  });
642
2185
  program2.command("entangle <sourceKey> <targetKey>").description("Link two secrets \u2014 rotating one updates the other").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
@@ -657,6 +2200,24 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
657
2200
  );
658
2201
  }
659
2202
  );
2203
+ 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(
2204
+ (sourceKey, targetKey, cmd) => {
2205
+ const sourceOpts = {
2206
+ scope: cmd.sourceProject ? "project" : "global",
2207
+ projectPath: cmd.sourceProject ?? process.cwd(),
2208
+ source: "cli"
2209
+ };
2210
+ const targetOpts = {
2211
+ scope: cmd.targetProject ? "project" : "global",
2212
+ projectPath: cmd.targetProject ?? process.cwd(),
2213
+ source: "cli"
2214
+ };
2215
+ disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
2216
+ console.log(
2217
+ `${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
2218
+ );
2219
+ }
2220
+ );
660
2221
  const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
661
2222
  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) => {
662
2223
  const id = tunnelCreate(value, {
@@ -836,6 +2397,36 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
836
2397
  }
837
2398
  console.log();
838
2399
  });
2400
+ program2.command("audit:verify").description("Verify the integrity of the audit hash chain").action(() => {
2401
+ const result = verifyAuditChain();
2402
+ if (result.totalEvents === 0) {
2403
+ console.log(c.dim(" No audit events to verify"));
2404
+ return;
2405
+ }
2406
+ if (result.intact) {
2407
+ console.log(`${SYMBOLS.shield} ${c.green("Audit chain intact")} \u2014 ${result.totalEvents} events verified`);
2408
+ } else {
2409
+ console.log(`${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`);
2410
+ console.log(c.dim(` ${result.validEvents}/${result.totalEvents} events valid before break`));
2411
+ if (result.brokenEvent) {
2412
+ console.log(c.dim(` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`));
2413
+ }
2414
+ process.exitCode = 1;
2415
+ }
2416
+ });
2417
+ program2.command("audit:export").description("Export audit events in a portable format").option("--since <date>", "Start date (ISO 8601)").option("--until <date>", "End date (ISO 8601)").option("--format <fmt>", "Output format: jsonl, json, csv", "jsonl").option("-o, --output <file>", "Write to file instead of stdout").action((cmd) => {
2418
+ const output = exportAudit({
2419
+ since: cmd.since,
2420
+ until: cmd.until,
2421
+ format: cmd.format
2422
+ });
2423
+ if (cmd.output) {
2424
+ writeFileSync4(cmd.output, output);
2425
+ console.log(`${SYMBOLS.check} Exported to ${cmd.output}`);
2426
+ } else {
2427
+ console.log(output);
2428
+ }
2429
+ });
839
2430
  program2.command("health").description("Check the health of all secrets").option("-g, --global", "Check global scope only").option("-p, --project", "Check project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
840
2431
  const opts = buildOpts(cmd);
841
2432
  const entries = listSecrets(opts);
@@ -884,8 +2475,166 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
884
2475
  }
885
2476
  console.log();
886
2477
  });
2478
+ const hook = program2.command("hook").description("Manage secret change hooks (callbacks on write/delete/rotate)");
2479
+ 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) => {
2480
+ let type;
2481
+ if (cmd.exec) type = "shell";
2482
+ else if (cmd.url) type = "http";
2483
+ else if (cmd.signalTarget) type = "signal";
2484
+ else {
2485
+ console.error(c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`));
2486
+ process.exit(1);
2487
+ }
2488
+ if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
2489
+ console.error(c.red(`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`));
2490
+ process.exit(1);
2491
+ }
2492
+ const actions = cmd.action.split(",").map((a) => a.trim());
2493
+ const entry = registerHook({
2494
+ type,
2495
+ match: {
2496
+ key: cmd.key,
2497
+ keyPattern: cmd.keyPattern,
2498
+ tag: cmd.tag,
2499
+ scope: cmd.scope,
2500
+ action: actions
2501
+ },
2502
+ command: cmd.exec,
2503
+ url: cmd.url,
2504
+ signal: cmd.signalTarget ? { target: cmd.signalTarget, signal: cmd.signalName } : void 0,
2505
+ description: cmd.description,
2506
+ enabled: true
2507
+ });
2508
+ console.log(`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`);
2509
+ if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
2510
+ if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
2511
+ if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
2512
+ });
2513
+ hook.command("list").alias("ls").description("List all registered hooks").action(() => {
2514
+ const hooks = listHooks();
2515
+ if (hooks.length === 0) {
2516
+ console.log(c.dim("No hooks registered"));
2517
+ return;
2518
+ }
2519
+ console.log(c.bold(`
2520
+ ${SYMBOLS.zap} Registered hooks (${hooks.length})
2521
+ `));
2522
+ for (const h of hooks) {
2523
+ const status = h.enabled ? c.green("on") : c.red("off");
2524
+ const matchParts = [];
2525
+ if (h.match.key) matchParts.push(`key=${h.match.key}`);
2526
+ if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
2527
+ if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
2528
+ if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
2529
+ if (h.match.action?.length) matchParts.push(`actions=${h.match.action.join(",")}`);
2530
+ const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
2531
+ console.log(` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`);
2532
+ console.log(` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`);
2533
+ }
2534
+ console.log();
2535
+ });
2536
+ hook.command("remove <id>").alias("rm").description("Remove a hook by ID").action((id) => {
2537
+ if (removeHook(id)) {
2538
+ console.log(`${SYMBOLS.check} ${c.green("removed")} hook ${c.bold(id)}`);
2539
+ } else {
2540
+ console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
2541
+ process.exit(1);
2542
+ }
2543
+ });
2544
+ hook.command("enable <id>").description("Enable a hook").action((id) => {
2545
+ if (enableHook(id)) {
2546
+ console.log(`${SYMBOLS.check} ${c.green("enabled")} hook ${c.bold(id)}`);
2547
+ } else {
2548
+ console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
2549
+ process.exit(1);
2550
+ }
2551
+ });
2552
+ hook.command("disable <id>").description("Disable a hook").action((id) => {
2553
+ if (disableHook(id)) {
2554
+ console.log(`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`);
2555
+ } else {
2556
+ console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
2557
+ process.exit(1);
2558
+ }
2559
+ });
2560
+ hook.command("test <id>").description("Dry-run a hook with a mock payload").action(async (id) => {
2561
+ const hooks = listHooks();
2562
+ const h = hooks.find((hook2) => hook2.id === id);
2563
+ if (!h) {
2564
+ console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
2565
+ process.exit(1);
2566
+ }
2567
+ console.log(c.dim(`Testing hook ${id} (${h.type})...
2568
+ `));
2569
+ const payload = {
2570
+ action: "write",
2571
+ key: h.match.key ?? "TEST_KEY",
2572
+ scope: h.match.scope ?? "global",
2573
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2574
+ source: "cli"
2575
+ };
2576
+ const results = await fireHooks(payload);
2577
+ const result = results.find((r) => r.hookId === id);
2578
+ if (result) {
2579
+ const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
2580
+ console.log(` ${icon} ${result.message}`);
2581
+ } else {
2582
+ console.log(c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`));
2583
+ }
2584
+ console.log();
2585
+ });
2586
+ 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) => {
2587
+ const projectPath = cmd.projectPath ?? process.cwd();
2588
+ const config = readProjectConfig(projectPath);
2589
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
2590
+ console.error(
2591
+ c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
2592
+ );
2593
+ process.exit(1);
2594
+ }
2595
+ const opts = buildOpts(cmd);
2596
+ const lines = [];
2597
+ const warnings = [];
2598
+ for (const [key, manifest] of Object.entries(config.secrets)) {
2599
+ const value = getSecret(key, { ...opts, projectPath, source: "cli" });
2600
+ if (value === null) {
2601
+ if (manifest.required !== false) {
2602
+ warnings.push(`MISSING (required): ${key}`);
2603
+ }
2604
+ lines.push(`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`);
2605
+ continue;
2606
+ }
2607
+ const result = getEnvelope(key, { projectPath, source: "cli" });
2608
+ if (result) {
2609
+ const decay = checkDecay(result.envelope);
2610
+ if (decay.isExpired) {
2611
+ warnings.push(`EXPIRED: ${key}`);
2612
+ } else if (decay.isStale) {
2613
+ warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
2614
+ }
2615
+ }
2616
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
2617
+ lines.push(`${key}="${escaped}"`);
2618
+ }
2619
+ const output = lines.join("\n") + "\n";
2620
+ if (cmd.output) {
2621
+ writeFileSync4(cmd.output, output);
2622
+ console.log(
2623
+ `${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
2624
+ );
2625
+ } else {
2626
+ process.stdout.write(output);
2627
+ }
2628
+ if (warnings.length > 0 && process.stderr.isTTY) {
2629
+ console.error();
2630
+ for (const w of warnings) {
2631
+ console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
2632
+ }
2633
+ console.error();
2634
+ }
2635
+ });
887
2636
  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) => {
888
- const { startDashboardServer } = await import("./dashboard-X3ONQFLV.js");
2637
+ const { startDashboardServer } = await import("./dashboard-JT5ZNLT5.js");
889
2638
  const { exec } = await import("child_process");
890
2639
  const { platform } = await import("os");
891
2640
  const port = Number(cmd.port);
@@ -937,6 +2686,96 @@ ${c.dim(" dashboard stopped")}`);
937
2686
  verbose: cmd.verbose
938
2687
  });
939
2688
  });
2689
+ program2.command("rotate <key>").description("Attempt issuer-native rotation of a secret via its provider").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider").action(async (key, cmd) => {
2690
+ const opts = buildOpts(cmd);
2691
+ const value = getSecret(key, opts);
2692
+ if (!value) {
2693
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
2694
+ process.exitCode = 1;
2695
+ return;
2696
+ }
2697
+ const result = await rotateWithProvider(value, cmd.provider);
2698
+ if (result.rotated && result.newValue) {
2699
+ setSecret(key, result.newValue, { ...opts, scope: opts.scope ?? "global" });
2700
+ console.log(`${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`);
2701
+ console.log(c.dim(` ${result.message}`));
2702
+ } else {
2703
+ console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
2704
+ }
2705
+ });
2706
+ program2.command("ci:validate").description("CI-oriented batch validation of all secrets (exit code 1 on failure)").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action(async (cmd) => {
2707
+ const opts = buildOpts(cmd);
2708
+ const entries = listSecrets(opts);
2709
+ const secrets = entries.map((e) => {
2710
+ const val = getSecret(e.key, { ...opts, scope: e.scope, silent: true });
2711
+ if (!val) return null;
2712
+ const provider = e.envelope?.meta.provider;
2713
+ const validationUrl = e.envelope?.meta.validationUrl;
2714
+ return { key: e.key, value: val, provider, validationUrl };
2715
+ }).filter((s) => s !== null);
2716
+ if (secrets.length === 0) {
2717
+ console.log(c.dim("No secrets to validate"));
2718
+ return;
2719
+ }
2720
+ const report = await ciValidateBatch(secrets);
2721
+ if (cmd.json) {
2722
+ console.log(JSON.stringify(report, null, 2));
2723
+ } else {
2724
+ console.log(c.bold(`
2725
+ CI Secret Validation: ${report.results.length} secrets
2726
+ `));
2727
+ for (const r of report.results) {
2728
+ const icon = r.validation.valid ? SYMBOLS.check : SYMBOLS.cross;
2729
+ const color = r.validation.valid ? c.green : c.red;
2730
+ console.log(` ${icon} ${color(r.key)} [${r.validation.provider}] ${r.validation.message}`);
2731
+ if (r.requiresRotation) console.log(c.dim(` \u2192 rotation required`));
2732
+ }
2733
+ console.log();
2734
+ if (!report.allValid) {
2735
+ console.log(c.red(` ${report.failCount} secret(s) failed validation`));
2736
+ process.exitCode = 1;
2737
+ } else {
2738
+ console.log(c.green(` All secrets valid`));
2739
+ }
2740
+ }
2741
+ });
2742
+ program2.command("policy").description("Show project governance policy summary").option("--json", "Output as JSON").action((cmd) => {
2743
+ const summary = getPolicySummary();
2744
+ if (cmd.json) {
2745
+ console.log(JSON.stringify(summary, null, 2));
2746
+ return;
2747
+ }
2748
+ console.log(c.bold("\n\u2696 Governance Policy\n"));
2749
+ if (!summary.hasMcpPolicy && !summary.hasExecPolicy && !summary.hasSecretPolicy) {
2750
+ console.log(c.dim(" No policy configured in .q-ring.json"));
2751
+ console.log(c.dim(' Add a "policy" section to enable governance controls.\n'));
2752
+ return;
2753
+ }
2754
+ if (summary.hasMcpPolicy) {
2755
+ console.log(c.cyan(" MCP Policy:"));
2756
+ const m = summary.details.mcp;
2757
+ if (m.allowTools) console.log(c.green(` Allow tools: ${m.allowTools.join(", ")}`));
2758
+ if (m.denyTools) console.log(c.red(` Deny tools: ${m.denyTools.join(", ")}`));
2759
+ if (m.readableKeys) console.log(c.green(` Readable keys: ${m.readableKeys.join(", ")}`));
2760
+ if (m.deniedKeys) console.log(c.red(` Denied keys: ${m.deniedKeys.join(", ")}`));
2761
+ if (m.deniedTags) console.log(c.red(` Denied tags: ${m.deniedTags.join(", ")}`));
2762
+ }
2763
+ if (summary.hasExecPolicy) {
2764
+ console.log(c.cyan(" Exec Policy:"));
2765
+ const e = summary.details.exec;
2766
+ if (e.allowCommands) console.log(c.green(` Allow commands: ${e.allowCommands.join(", ")}`));
2767
+ if (e.denyCommands) console.log(c.red(` Deny commands: ${e.denyCommands.join(", ")}`));
2768
+ if (e.maxRuntimeSeconds) console.log(` Max runtime: ${e.maxRuntimeSeconds}s`);
2769
+ if (e.allowNetwork !== void 0) console.log(` Allow network: ${e.allowNetwork}`);
2770
+ }
2771
+ if (summary.hasSecretPolicy) {
2772
+ console.log(c.cyan(" Secret Lifecycle Policy:"));
2773
+ const s = summary.details.secrets;
2774
+ if (s.requireApprovalForTags) console.log(` Require approval for tags: ${s.requireApprovalForTags.join(", ")}`);
2775
+ if (s.maxTtlSeconds) console.log(` Max TTL: ${s.maxTtlSeconds}s`);
2776
+ }
2777
+ console.log();
2778
+ });
940
2779
  return program2;
941
2780
  }
942
2781