@i4ctime/q-ring 0.4.0 → 0.9.2

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,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  checkDecay,
4
+ checkExecPolicy,
4
5
  collapseEnvironment,
5
6
  deleteSecret,
6
7
  detectAnomalies,
@@ -8,24 +9,33 @@ import {
8
9
  disentangleSecrets,
9
10
  enableHook,
10
11
  entangleSecrets,
12
+ exportAudit,
11
13
  exportSecrets,
12
14
  fireHooks,
13
15
  getEnvelope,
16
+ getExecMaxRuntime,
17
+ getPolicySummary,
14
18
  getSecret,
19
+ grantApproval,
15
20
  hasSecret,
21
+ httpRequest_,
22
+ listApprovals,
16
23
  listHooks,
17
24
  listSecrets,
18
25
  logAudit,
19
26
  queryAudit,
20
27
  readProjectConfig,
21
28
  registerHook,
29
+ registry,
22
30
  removeHook,
31
+ revokeApproval,
23
32
  setSecret,
24
33
  tunnelCreate,
25
34
  tunnelDestroy,
26
35
  tunnelList,
27
- tunnelRead
28
- } from "./chunk-IGNU622R.js";
36
+ tunnelRead,
37
+ verifyAuditChain
38
+ } from "./chunk-WG4ZKN7Q.js";
29
39
 
30
40
  // src/cli/commands.ts
31
41
  import { Command } from "commander";
@@ -432,33 +442,443 @@ function importDotenv(filePathOrContent, options = {}) {
432
442
  return result;
433
443
  }
434
444
 
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
- );
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);
452
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))
453
558
  );
454
- req.on("error", reject);
455
- req.on("timeout", () => {
456
- req.destroy();
457
- reject(new Error("Request timed out"));
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);
458
640
  });
459
- req.end();
460
641
  });
461
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
+ }
462
882
  var ProviderRegistry = class {
463
883
  providers = /* @__PURE__ */ new Map();
464
884
  register(provider) {
@@ -605,14 +1025,14 @@ var httpProvider = {
605
1025
  }
606
1026
  }
607
1027
  };
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);
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);
614
1034
  async function validateSecret(value, opts) {
615
- const provider = opts?.provider ? registry.get(opts.provider) : registry.detectProvider(value);
1035
+ const provider = opts?.provider ? registry2.get(opts.provider) : registry2.detectProvider(value);
616
1036
  if (!provider) {
617
1037
  return {
618
1038
  valid: false,
@@ -627,9 +1047,271 @@ async function validateSecret(value, opts) {
627
1047
  }
628
1048
  return provider.validate(value);
629
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
+ }
630
1312
 
631
1313
  // src/cli/commands.ts
632
- import { writeFileSync } from "fs";
1314
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
633
1315
 
634
1316
  // src/utils/prompt.ts
635
1317
  import { createInterface } from "readline";
@@ -683,6 +1365,8 @@ function buildOpts(cmd) {
683
1365
  let scope;
684
1366
  if (cmd.global) scope = "global";
685
1367
  else if (cmd.project) scope = "project";
1368
+ else if (cmd.team) scope = "team";
1369
+ else if (cmd.org) scope = "org";
686
1370
  const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
687
1371
  if (scope === "project" && !projectPath) {
688
1372
  throw new Error("Project path is required for project scope");
@@ -690,6 +1374,8 @@ function buildOpts(cmd) {
690
1374
  return {
691
1375
  scope,
692
1376
  projectPath: projectPath ?? process.cwd(),
1377
+ teamId: cmd.team,
1378
+ orgId: cmd.org,
693
1379
  env: cmd.env,
694
1380
  source: "cli"
695
1381
  };
@@ -698,7 +1384,7 @@ function createProgram() {
698
1384
  const program2 = new Command().name("qring").description(
699
1385
  `${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
700
1386
  ).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) => {
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) => {
702
1388
  const opts = buildOpts(cmd);
703
1389
  if (!value) {
704
1390
  value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
@@ -714,7 +1400,9 @@ function createProgram() {
714
1400
  description: cmd.description,
715
1401
  tags: cmd.tags?.split(",").map((t) => t.trim()),
716
1402
  rotationFormat: cmd.rotationFormat,
717
- rotationPrefix: cmd.rotationPrefix
1403
+ rotationPrefix: cmd.rotationPrefix,
1404
+ requiresApproval: cmd.requiresApproval,
1405
+ jitProvider: cmd.jitProvider
718
1406
  };
719
1407
  if (cmd.env) {
720
1408
  const existing = getEnvelope(key, opts);
@@ -739,7 +1427,7 @@ function createProgram() {
739
1427
  );
740
1428
  }
741
1429
  });
742
- 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) => {
743
1431
  const opts = buildOpts(cmd);
744
1432
  const value = getSecret(key, opts);
745
1433
  if (value === null) {
@@ -758,7 +1446,7 @@ function createProgram() {
758
1446
  process.exit(1);
759
1447
  }
760
1448
  });
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) => {
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) => {
762
1450
  const opts = buildOpts(cmd);
763
1451
  let entries = listSecrets(opts);
764
1452
  if (cmd.tag) {
@@ -1020,7 +1708,7 @@ function createProgram() {
1020
1708
  console.log(c.bold(`
1021
1709
  ${SYMBOLS.shield} Available validation providers
1022
1710
  `));
1023
- for (const p of registry.listProviders()) {
1711
+ for (const p of registry2.listProviders()) {
1024
1712
  const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
1025
1713
  console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
1026
1714
  }
@@ -1087,6 +1775,372 @@ function createProgram() {
1087
1775
  }
1088
1776
  console.log();
1089
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
+ });
1090
2144
  program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
1091
2145
  const result = collapseEnvironment({
1092
2146
  projectPath: cmd.projectPath ?? process.cwd()
@@ -1343,6 +2397,36 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
1343
2397
  }
1344
2398
  console.log();
1345
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
+ });
1346
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) => {
1347
2431
  const opts = buildOpts(cmd);
1348
2432
  const entries = listSecrets(opts);
@@ -1534,7 +2618,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
1534
2618
  }
1535
2619
  const output = lines.join("\n") + "\n";
1536
2620
  if (cmd.output) {
1537
- writeFileSync(cmd.output, output);
2621
+ writeFileSync4(cmd.output, output);
1538
2622
  console.log(
1539
2623
  `${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
1540
2624
  );
@@ -1550,7 +2634,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
1550
2634
  }
1551
2635
  });
1552
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) => {
1553
- const { startDashboardServer } = await import("./dashboard-HVIQO6NT.js");
2637
+ const { startDashboardServer } = await import("./dashboard-JT5ZNLT5.js");
1554
2638
  const { exec } = await import("child_process");
1555
2639
  const { platform } = await import("os");
1556
2640
  const port = Number(cmd.port);
@@ -1602,6 +2686,96 @@ ${c.dim(" dashboard stopped")}`);
1602
2686
  verbose: cmd.verbose
1603
2687
  });
1604
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
+ });
1605
2779
  return program2;
1606
2780
  }
1607
2781