@i4ctime/q-ring 0.9.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ PACKAGE_VERSION,
3
4
  checkDecay,
4
5
  checkExecPolicy,
5
6
  checkSSRF,
7
+ clearMemory,
6
8
  collapseEnvironment,
7
9
  deleteSecret,
8
10
  detectAnomalies,
@@ -13,21 +15,25 @@ import {
13
15
  exportAudit,
14
16
  exportSecrets,
15
17
  fireHooks,
18
+ forget,
16
19
  getEnvelope,
17
20
  getExecMaxRuntime,
18
21
  getPolicySummary,
19
22
  getSecret,
20
23
  grantApproval,
21
24
  hasSecret,
22
- httpRequest_,
25
+ httpRequest,
23
26
  listApprovals,
24
27
  listHooks,
28
+ listMemory,
25
29
  listSecrets,
26
30
  logAudit,
27
31
  queryAudit,
28
32
  readProjectConfig,
33
+ recall,
29
34
  registerHook,
30
35
  registry,
36
+ remember,
31
37
  removeHook,
32
38
  revokeApproval,
33
39
  setSecret,
@@ -36,93 +42,10 @@ import {
36
42
  tunnelList,
37
43
  tunnelRead,
38
44
  verifyAuditChain
39
- } from "./chunk-JV4JFWV6.js";
45
+ } from "./chunk-A4RZVP3P.js";
40
46
 
41
47
  // src/cli/commands.ts
42
- import { Command } from "commander";
43
-
44
- // src/core/noise.ts
45
- import { randomBytes, randomInt } from "crypto";
46
- var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
47
- var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
48
- function randomString(charset, length) {
49
- let result = "";
50
- for (let i = 0; i < length; i++) {
51
- result += charset[randomInt(charset.length)];
52
- }
53
- return result;
54
- }
55
- function generateSecret(opts = {}) {
56
- const format = opts.format ?? "api-key";
57
- switch (format) {
58
- case "hex": {
59
- const len = opts.length ?? 32;
60
- return randomBytes(len).toString("hex");
61
- }
62
- case "base64": {
63
- const len = opts.length ?? 32;
64
- return randomBytes(len).toString("base64url");
65
- }
66
- case "alphanumeric": {
67
- const len = opts.length ?? 32;
68
- return randomString(ALPHA_NUM, len);
69
- }
70
- case "uuid": {
71
- const bytes = randomBytes(16);
72
- bytes[6] = bytes[6] & 15 | 64;
73
- bytes[8] = bytes[8] & 63 | 128;
74
- const hex = bytes.toString("hex");
75
- return [
76
- hex.slice(0, 8),
77
- hex.slice(8, 12),
78
- hex.slice(12, 16),
79
- hex.slice(16, 20),
80
- hex.slice(20, 32)
81
- ].join("-");
82
- }
83
- case "api-key": {
84
- const prefix = opts.prefix ?? "qr_";
85
- const len = opts.length ?? 48;
86
- return prefix + randomString(ALPHA_NUM, len);
87
- }
88
- case "token": {
89
- const prefix = opts.prefix ?? "";
90
- const len = opts.length ?? 64;
91
- return prefix + randomBytes(len).toString("base64url");
92
- }
93
- case "password": {
94
- const len = opts.length ?? 24;
95
- let pw = randomString(PASSWORD_CHARS, len);
96
- const hasUpper = /[A-Z]/.test(pw);
97
- const hasLower = /[a-z]/.test(pw);
98
- const hasDigit = /[0-9]/.test(pw);
99
- const hasSpecial = /[^A-Za-z0-9]/.test(pw);
100
- if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
101
- if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
102
- if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
103
- if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
104
- return pw;
105
- }
106
- default:
107
- return randomBytes(32).toString("hex");
108
- }
109
- }
110
- function replaceAt(str, index, char) {
111
- return str.slice(0, index) + char + str.slice(index + 1);
112
- }
113
- function estimateEntropy(secret) {
114
- const charsets = [
115
- { regex: /[a-z]/, size: 26 },
116
- { regex: /[A-Z]/, size: 26 },
117
- { regex: /[0-9]/, size: 10 },
118
- { regex: /[^A-Za-z0-9]/, size: 32 }
119
- ];
120
- let poolSize = 0;
121
- for (const { regex, size } of charsets) {
122
- if (regex.test(secret)) poolSize += size;
123
- }
124
- return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
125
- }
48
+ import { Command, Help } from "commander";
126
49
 
127
50
  // src/utils/colors.ts
128
51
  var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
@@ -191,783 +114,600 @@ var SYMBOLS = {
191
114
  sparkle: enabled ? "\u2728" : "[*]"
192
115
  };
193
116
 
194
- // src/core/agent.ts
195
- function defaultConfig() {
196
- return {
197
- intervalSeconds: 60,
198
- autoRotate: false,
199
- projectPaths: [process.cwd()],
200
- verbose: false
201
- };
202
- }
203
- function runHealthScan(config = {}) {
204
- const cfg = { ...defaultConfig(), ...config };
205
- const report = {
206
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
207
- totalSecrets: 0,
208
- healthy: 0,
209
- stale: 0,
210
- expired: 0,
211
- anomalies: 0,
212
- rotated: [],
213
- warnings: []
214
- };
215
- const globalEntries = listSecrets({ scope: "global", source: "agent" });
216
- const projectEntries = cfg.projectPaths.flatMap(
217
- (pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
218
- );
219
- const allEntries = [...globalEntries, ...projectEntries];
220
- report.totalSecrets = allEntries.length;
221
- for (const entry of allEntries) {
222
- if (!entry.envelope) continue;
223
- const decay = checkDecay(entry.envelope);
224
- if (decay.isExpired) {
225
- report.expired++;
226
- report.warnings.push(
227
- `EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
228
- );
229
- if (cfg.autoRotate) {
230
- const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
231
- const prefix = entry.envelope?.meta.rotationPrefix;
232
- const newValue = generateSecret({ format: fmt, prefix });
233
- setSecret(entry.key, newValue, {
234
- scope: entry.scope,
235
- projectPath: cfg.projectPaths[0],
236
- source: "agent"
237
- });
238
- report.rotated.push(entry.key);
239
- logAudit({
240
- action: "write",
241
- key: entry.key,
242
- scope: entry.scope,
243
- source: "agent",
244
- detail: "auto-rotated by agent (expired)"
245
- });
246
- fireHooks({
247
- action: "rotate",
248
- key: entry.key,
249
- scope: entry.scope,
250
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
251
- source: "agent"
252
- }, entry.envelope?.meta.tags).catch(() => {
253
- });
254
- }
255
- } else if (decay.isStale) {
256
- report.stale++;
257
- report.warnings.push(
258
- `STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
259
- );
260
- } else {
261
- report.healthy++;
117
+ // src/core/import.ts
118
+ import { readFileSync } from "fs";
119
+ function parseDotenv(content) {
120
+ const result = /* @__PURE__ */ new Map();
121
+ const lines = content.split(/\r?\n/);
122
+ for (let i = 0; i < lines.length; i++) {
123
+ const line = lines[i].trim();
124
+ if (!line || line.startsWith("#")) continue;
125
+ const eqIdx = line.indexOf("=");
126
+ if (eqIdx === -1) continue;
127
+ const key = line.slice(0, eqIdx).trim();
128
+ let value = line.slice(eqIdx + 1).trim();
129
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
130
+ value = value.slice(1, -1);
262
131
  }
132
+ const escapeMap = {
133
+ n: "\n",
134
+ r: "\r",
135
+ t: " ",
136
+ "\\": "\\",
137
+ '"': '"'
138
+ };
139
+ value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
140
+ if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
141
+ value = value.split("#")[0].trim();
142
+ }
143
+ if (key) result.set(key, value);
263
144
  }
264
- const anomalies = detectAnomalies();
265
- report.anomalies = anomalies.length;
266
- for (const a of anomalies) {
267
- report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
268
- }
269
- return report;
145
+ return result;
270
146
  }
271
- function formatReport(report, verbose) {
272
- const lines = [];
273
- lines.push(
274
- `${c.bold(`${SYMBOLS.shield} q-ring agent scan`)} ${c.dim(report.timestamp)}`
275
- );
276
- lines.push(
277
- ` ${c.dim("secrets:")} ${report.totalSecrets} ${c.green(`${SYMBOLS.check} ${report.healthy}`)} ${c.yellow(`${SYMBOLS.warning} ${report.stale}`)} ${c.red(`${SYMBOLS.cross} ${report.expired}`)} ${c.dim(`anomalies: ${report.anomalies}`)}`
278
- );
279
- if (report.rotated.length > 0) {
280
- lines.push(
281
- ` ${c.cyan(`${SYMBOLS.zap} auto-rotated:`)} ${report.rotated.join(", ")}`
282
- );
147
+ function importDotenv(filePathOrContent, options = {}) {
148
+ let content;
149
+ try {
150
+ content = readFileSync(filePathOrContent, "utf8");
151
+ } catch {
152
+ content = filePathOrContent;
283
153
  }
284
- if (verbose && report.warnings.length > 0) {
285
- lines.push("");
286
- for (const w of report.warnings) {
287
- if (w.startsWith("EXPIRED")) lines.push(` ${c.red(w)}`);
288
- else if (w.startsWith("STALE")) lines.push(` ${c.yellow(w)}`);
289
- else if (w.startsWith("ANOMALY")) lines.push(` ${c.magenta(w)}`);
290
- else lines.push(` ${w}`);
154
+ const pairs = parseDotenv(content);
155
+ const result = {
156
+ imported: [],
157
+ skipped: [],
158
+ total: pairs.size
159
+ };
160
+ for (const [key, value] of pairs) {
161
+ if (options.skipExisting && hasSecret(key, {
162
+ scope: options.scope,
163
+ projectPath: options.projectPath,
164
+ source: options.source ?? "cli"
165
+ })) {
166
+ result.skipped.push(key);
167
+ continue;
168
+ }
169
+ if (options.dryRun) {
170
+ result.imported.push(key);
171
+ continue;
291
172
  }
173
+ const setOpts = {
174
+ scope: options.scope ?? "global",
175
+ projectPath: options.projectPath ?? process.cwd(),
176
+ source: options.source ?? "cli"
177
+ };
178
+ setSecret(key, value, setOpts);
179
+ result.imported.push(key);
292
180
  }
293
- return lines.join("\n");
181
+ return result;
294
182
  }
295
- async function startAgent(config = {}) {
296
- const cfg = { ...defaultConfig(), ...config };
297
- console.log(
298
- `${c.bold(`${SYMBOLS.zap} q-ring agent started`)} ${c.dim(`(interval: ${cfg.intervalSeconds}s, auto-rotate: ${cfg.autoRotate})`)}`
299
- );
300
- console.log(
301
- c.dim(` monitoring: global + ${cfg.projectPaths.length} project(s)`)
302
- );
303
- console.log();
304
- const scan = () => {
305
- const report = runHealthScan(cfg);
306
- console.log(formatReport(report, cfg.verbose));
307
- if (report.warnings.length > 0 || cfg.verbose) {
308
- console.log();
309
- }
310
- };
311
- scan();
312
- const interval = setInterval(scan, cfg.intervalSeconds * 1e3);
313
- const shutdown = () => {
314
- clearInterval(interval);
315
- console.log(`
316
- ${c.dim("q-ring agent stopped")}`);
317
- process.exit(0);
318
- };
319
- process.on("SIGINT", shutdown);
320
- process.on("SIGTERM", shutdown);
321
- await new Promise(() => {
183
+
184
+ // src/utils/prompt.ts
185
+ import { createInterface } from "readline";
186
+ async function promptSecret(message) {
187
+ const rl = createInterface({
188
+ input: process.stdin,
189
+ output: process.stderr,
190
+ terminal: true
191
+ });
192
+ return new Promise((resolve) => {
193
+ process.stderr.write(message);
194
+ const stdin = process.stdin;
195
+ const wasRaw = stdin.isRaw;
196
+ if (stdin.isTTY && stdin.setRawMode) {
197
+ stdin.setRawMode(true);
198
+ }
199
+ let input = "";
200
+ const onData = (chunk) => {
201
+ const char = chunk.toString("utf8");
202
+ if (char === "\n" || char === "\r") {
203
+ stdin.removeListener("data", onData);
204
+ if (stdin.isTTY && stdin.setRawMode) {
205
+ stdin.setRawMode(wasRaw ?? false);
206
+ }
207
+ process.stderr.write("\n");
208
+ rl.close();
209
+ resolve(input);
210
+ } else if (char === "") {
211
+ process.exit(1);
212
+ } else if (char === "\x7F" || char === "\b") {
213
+ input = input.slice(0, -1);
214
+ } else {
215
+ input += char;
216
+ }
217
+ };
218
+ stdin.on("data", onData);
322
219
  });
323
220
  }
324
221
 
325
- // src/core/teleport.ts
326
- import {
327
- randomBytes as randomBytes2,
328
- createCipheriv,
329
- createDecipheriv,
330
- pbkdf2Sync
331
- } from "crypto";
332
- var ALGORITHM = "aes-256-gcm";
333
- var KEY_LENGTH = 32;
334
- var IV_LENGTH = 16;
335
- var SALT_LENGTH = 32;
336
- var PBKDF2_ITERATIONS = 1e5;
337
- function deriveKey(passphrase, salt) {
338
- return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
222
+ // src/cli/helpers.ts
223
+ function wantsJsonOutput(program2, cmd) {
224
+ const c2 = cmd;
225
+ if (c2.json) return true;
226
+ const g = program2.opts();
227
+ return !!g.json;
339
228
  }
340
- function teleportPack(secrets, passphrase) {
341
- const payload = {
342
- secrets,
343
- exportedAt: (/* @__PURE__ */ new Date()).toISOString()
344
- };
345
- const plaintext = JSON.stringify(payload);
346
- const salt = randomBytes2(SALT_LENGTH);
347
- const iv = randomBytes2(IV_LENGTH);
348
- const key = deriveKey(passphrase, salt);
349
- const cipher = createCipheriv(ALGORITHM, key, iv);
350
- const encrypted = Buffer.concat([
351
- cipher.update(plaintext, "utf8"),
352
- cipher.final()
353
- ]);
354
- const tag = cipher.getAuthTag();
355
- const bundle = {
356
- v: 1,
357
- data: encrypted.toString("base64"),
358
- salt: salt.toString("base64"),
359
- iv: iv.toString("base64"),
360
- tag: tag.toString("base64"),
361
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
362
- count: secrets.length
363
- };
364
- return Buffer.from(JSON.stringify(bundle)).toString("base64");
229
+ function safeStr(s) {
230
+ return s == null ? "" : `${s}`;
365
231
  }
366
- function teleportUnpack(encoded, passphrase) {
367
- const bundleJson = Buffer.from(encoded, "base64").toString("utf8");
368
- const bundle = JSON.parse(bundleJson);
369
- if (bundle.v !== 1) {
370
- throw new Error(`Unsupported teleport bundle version: ${bundle.v}`);
371
- }
372
- const salt = Buffer.from(bundle.salt, "base64");
373
- const iv = Buffer.from(bundle.iv, "base64");
374
- const tag = Buffer.from(bundle.tag, "base64");
375
- const encrypted = Buffer.from(bundle.data, "base64");
376
- const key = deriveKey(passphrase, salt);
377
- const decipher = createDecipheriv(ALGORITHM, key, iv);
378
- decipher.setAuthTag(tag);
379
- const decrypted = Buffer.concat([
380
- decipher.update(encrypted),
381
- decipher.final()
382
- ]);
383
- return JSON.parse(decrypted.toString("utf8"));
232
+ function safeNum(n) {
233
+ return n == null ? 0 : Number(n);
384
234
  }
385
-
386
- // src/core/import.ts
387
- import { readFileSync } from "fs";
388
- function parseDotenv(content) {
389
- const result = /* @__PURE__ */ new Map();
390
- const lines = content.split(/\r?\n/);
391
- for (let i = 0; i < lines.length; i++) {
392
- const line = lines[i].trim();
393
- if (!line || line.startsWith("#")) continue;
394
- const eqIdx = line.indexOf("=");
395
- if (eqIdx === -1) continue;
396
- const key = line.slice(0, eqIdx).trim();
397
- let value = line.slice(eqIdx + 1).trim();
398
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
399
- value = value.slice(1, -1);
400
- }
401
- const escapeMap = {
402
- n: "\n",
403
- r: "\r",
404
- t: " ",
405
- "\\": "\\",
406
- '"': '"'
407
- };
408
- value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
409
- if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
410
- value = value.split("#")[0].trim();
411
- }
412
- if (key) result.set(key, value);
413
- }
414
- return result;
235
+ function safeArr(arr) {
236
+ return arr ? arr.map((x) => typeof x === "string" ? safeStr(x) : x) : [];
415
237
  }
416
- function importDotenv(filePathOrContent, options = {}) {
417
- let content;
418
- try {
419
- content = readFileSync(filePathOrContent, "utf8");
420
- } catch {
421
- content = filePathOrContent;
238
+
239
+ // src/cli/options.ts
240
+ function buildOpts(cmd) {
241
+ let scope;
242
+ if (cmd.global) scope = "global";
243
+ else if (cmd.project) scope = "project";
244
+ else if (cmd.team) scope = "team";
245
+ else if (cmd.org) scope = "org";
246
+ const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
247
+ if (scope === "project" && !projectPath) {
248
+ throw new Error("Project path is required for project scope");
422
249
  }
423
- const pairs = parseDotenv(content);
424
- const result = {
425
- imported: [],
426
- skipped: [],
427
- total: pairs.size
250
+ return {
251
+ scope,
252
+ projectPath: projectPath ?? process.cwd(),
253
+ teamId: cmd.team,
254
+ orgId: cmd.org,
255
+ env: cmd.env,
256
+ source: "cli"
428
257
  };
429
- for (const [key, value] of pairs) {
430
- if (options.skipExisting && hasSecret(key, {
431
- scope: options.scope,
432
- projectPath: options.projectPath,
433
- source: options.source ?? "cli"
434
- })) {
435
- result.skipped.push(key);
436
- continue;
437
- }
438
- if (options.dryRun) {
439
- result.imported.push(key);
440
- continue;
441
- }
442
- const setOpts = {
443
- scope: options.scope ?? "global",
444
- projectPath: options.projectPath ?? process.cwd(),
445
- source: options.source ?? "cli"
446
- };
447
- setSecret(key, value, setOpts);
448
- result.imported.push(key);
449
- }
450
- return result;
451
258
  }
452
259
 
453
- // src/core/exec.ts
454
- import { spawn } from "child_process";
455
- import { Transform } from "stream";
456
- var BUILTIN_PROFILES = {
457
- unrestricted: { name: "unrestricted" },
458
- restricted: {
459
- name: "restricted",
460
- denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
461
- maxRuntimeSeconds: 30,
462
- allowNetwork: false,
463
- stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
464
- },
465
- ci: {
466
- name: "ci",
467
- maxRuntimeSeconds: 300,
468
- allowNetwork: true,
469
- denyCommands: ["rm -rf /", "mkfs", "dd if="]
260
+ // src/services/list-secrets-filter.ts
261
+ function globKeyPatternToRegexSource(pattern) {
262
+ let out = "";
263
+ for (const c2 of pattern) {
264
+ if (c2 === "*") out += ".*";
265
+ else if (c2 === "?") out += ".";
266
+ else if ("\\^$+{}()|[]".includes(c2)) out += "\\" + c2;
267
+ else if (c2 === ".") out += "\\.";
268
+ else out += c2;
470
269
  }
471
- };
472
- function getProfile(name) {
473
- if (!name) return BUILTIN_PROFILES.unrestricted;
474
- return BUILTIN_PROFILES[name] ?? { name };
270
+ return out;
475
271
  }
476
- var RedactionTransform = class extends Transform {
477
- patterns = [];
478
- tail = "";
479
- maxLen = 0;
480
- constructor(secretsToRedact) {
481
- super();
482
- const validSecrets = secretsToRedact.filter((s) => s.length > 5);
483
- validSecrets.sort((a, b) => b.length - a.length);
484
- this.patterns = validSecrets.map((s) => ({
485
- value: s,
486
- replacement: "[QRING:REDACTED]"
487
- }));
488
- if (validSecrets.length > 0) {
489
- this.maxLen = validSecrets[0].length;
490
- }
491
- }
492
- _transform(chunk, encoding, callback) {
493
- if (this.patterns.length === 0) {
494
- this.push(chunk);
495
- return callback();
496
- }
497
- const text = this.tail + chunk.toString();
498
- let redacted = text;
499
- for (const { value, replacement } of this.patterns) {
500
- redacted = redacted.split(value).join(replacement);
501
- }
502
- if (redacted.length < this.maxLen) {
503
- this.tail = redacted;
504
- return callback();
505
- }
506
- const outputLen = redacted.length - this.maxLen + 1;
507
- const output = redacted.slice(0, outputLen);
508
- this.tail = redacted.slice(outputLen);
509
- this.push(output);
510
- callback();
511
- }
512
- _flush(callback) {
513
- if (this.tail) {
514
- let final = this.tail;
515
- for (const { value, replacement } of this.patterns) {
516
- final = final.split(value).join(replacement);
272
+ function filterSecretsByKeyGlob(entries, filter) {
273
+ if (!filter?.trim()) return entries;
274
+ const regex = new RegExp("^" + globKeyPatternToRegexSource(filter) + "$", "i");
275
+ return entries.filter((e) => regex.test(e.key));
276
+ }
277
+
278
+ // src/cli/commands/secrets.ts
279
+ function registerSecretsCommands(program2) {
280
+ 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(
281
+ "-e, --env <env>",
282
+ "Set value for a specific environment (superposition)"
283
+ ).option(
284
+ "--ttl <seconds>",
285
+ "Time-to-live in seconds (quantum decay)",
286
+ parseInt
287
+ ).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").option(
288
+ "--rotation-format <format>",
289
+ "Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)"
290
+ ).option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").option(
291
+ "--requires-approval",
292
+ "Require explicit user approval for MCP agents to read"
293
+ ).option(
294
+ "--jit-provider <provider>",
295
+ "Use a Just-In-Time provider to dynamically generate this secret"
296
+ ).action(async (key, value, cmd) => {
297
+ const opts = buildOpts(cmd);
298
+ if (!value) {
299
+ value = await promptSecret(
300
+ `${SYMBOLS.key} Enter value for ${c.bold(key)}: `
301
+ );
302
+ if (!value) {
303
+ console.error(c.red(`${SYMBOLS.cross} No value provided, aborting.`));
304
+ process.exit(1);
517
305
  }
518
- this.push(final);
519
306
  }
520
- callback();
521
- }
522
- };
523
- async function execCommand(opts) {
524
- const profile = getProfile(opts.profile);
525
- const fullCommand = [opts.command, ...opts.args].join(" ");
526
- const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
527
- if (!policyDecision.allowed) {
528
- throw new Error(`Policy Denied: ${policyDecision.reason}`);
529
- }
530
- if (profile.denyCommands) {
531
- const denied = profile.denyCommands.find((d) => {
532
- const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
533
- return pattern.test(fullCommand);
534
- });
535
- if (denied) {
536
- throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
307
+ const setOpts = {
308
+ ...opts,
309
+ ttlSeconds: cmd.ttl,
310
+ expiresAt: cmd.expires,
311
+ description: cmd.description,
312
+ tags: cmd.tags?.split(",").map((t) => t.trim()),
313
+ rotationFormat: cmd.rotationFormat,
314
+ rotationPrefix: cmd.rotationPrefix,
315
+ requiresApproval: cmd.requiresApproval,
316
+ jitProvider: cmd.jitProvider
317
+ };
318
+ if (cmd.env) {
319
+ const existing = getEnvelope(key, opts);
320
+ const states = existing?.envelope?.states ?? {};
321
+ states[cmd.env] = value;
322
+ if (existing?.envelope?.value && !states["default"]) {
323
+ states["default"] = existing.envelope.value;
324
+ }
325
+ setOpts.states = states;
326
+ setOpts.defaultEnv = existing?.envelope?.defaultEnv ?? cmd.env;
327
+ setSecret(key, "", setOpts);
328
+ console.log(
329
+ `${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${envBadge(cmd.env)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)}`
330
+ );
331
+ } else {
332
+ setSecret(key, value, setOpts);
333
+ const extras = [];
334
+ if (cmd.ttl) extras.push(`${SYMBOLS.clock} ttl=${cmd.ttl}s`);
335
+ if (cmd.description) extras.push(c.dim(cmd.description));
336
+ console.log(
337
+ `${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)} ${extras.join(" ")}`
338
+ );
537
339
  }
538
- }
539
- if (profile.allowCommands) {
540
- const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
541
- if (!allowed) {
542
- throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
340
+ });
341
+ 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").option("--raw", "Print the secret value only (for scripts and piping)").action((key, cmd) => {
342
+ const opts = buildOpts(cmd);
343
+ const value = getSecret(key, opts);
344
+ if (value === null) {
345
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
346
+ process.exit(1);
543
347
  }
544
- }
545
- const envMap = {};
546
- for (const [k, v] of Object.entries(process.env)) {
547
- if (v !== void 0) envMap[k] = v;
548
- }
549
- if (profile.stripEnvVars) {
550
- for (const key of profile.stripEnvVars) {
551
- delete envMap[key];
348
+ if (cmd.raw) {
349
+ process.stdout.write(value);
350
+ return;
552
351
  }
553
- }
554
- const secretsToRedact = /* @__PURE__ */ new Set();
555
- let entries = listSecrets({
556
- scope: opts.scope,
557
- projectPath: opts.projectPath,
558
- source: opts.source ?? "cli",
559
- silent: true
560
- // list silently
352
+ const payload = wantsJsonOutput(program2, cmd) ? { ok: true, data: { key, value } } : { key, value };
353
+ process.stdout.write(JSON.stringify(payload) + "\n");
561
354
  });
562
- if (opts.keys?.length) {
563
- const keySet = new Set(opts.keys);
564
- entries = entries.filter((e) => keySet.has(e.key));
565
- }
566
- if (opts.tags?.length) {
567
- entries = entries.filter(
568
- (e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
569
- );
570
- }
571
- for (const entry of entries) {
572
- if (entry.envelope) {
573
- const decay = checkDecay(entry.envelope);
574
- if (decay.isExpired) continue;
355
+ program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
356
+ const opts = buildOpts(cmd);
357
+ const deleted = deleteSecret(key, opts);
358
+ if (deleted) {
359
+ console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
360
+ } else {
361
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
362
+ process.exit(1);
575
363
  }
576
- const val = getSecret(entry.key, {
577
- scope: entry.scope,
578
- projectPath: opts.projectPath,
579
- env: opts.env,
580
- source: opts.source ?? "cli",
581
- silent: false
582
- // Log access for execution
583
- });
584
- if (val !== null) {
585
- envMap[entry.key] = val;
586
- if (val.length > 5) {
587
- secretsToRedact.add(val);
588
- }
364
+ });
365
+ 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) => {
366
+ const opts = buildOpts(cmd);
367
+ let entries = listSecrets(opts);
368
+ if (cmd.tag) {
369
+ entries = entries.filter(
370
+ (e) => e.envelope?.meta.tags?.includes(cmd.tag)
371
+ );
589
372
  }
590
- }
591
- const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
592
- return new Promise((resolve, reject) => {
593
- const networkTools = /* @__PURE__ */ new Set([
594
- "curl",
595
- "wget",
596
- "ping",
597
- "nc",
598
- "netcat",
599
- "ssh",
600
- "telnet",
601
- "ftp",
602
- "dig",
603
- "nslookup"
604
- ]);
605
- if (profile.allowNetwork === false && networkTools.has(opts.command)) {
606
- const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
607
- if (opts.captureOutput) {
608
- return resolve({ code: 126, stdout: "", stderr: msg });
609
- }
610
- process.stderr.write(msg + "\n");
611
- return resolve({ code: 126, stdout: "", stderr: "" });
373
+ if (cmd.expired) {
374
+ entries = entries.filter((e) => e.decay?.isExpired);
612
375
  }
613
- const child = spawn(opts.command, opts.args, {
614
- env: envMap,
615
- stdio: ["inherit", "pipe", "pipe"],
616
- shell: false
617
- });
618
- let timedOut = false;
619
- let timer;
620
- if (maxRuntime) {
621
- timer = setTimeout(() => {
622
- timedOut = true;
623
- child.kill("SIGKILL");
624
- }, maxRuntime * 1e3);
376
+ if (cmd.stale) {
377
+ entries = entries.filter(
378
+ (e) => e.decay?.isStale && !e.decay?.isExpired
379
+ );
625
380
  }
626
- const stdoutRedact = new RedactionTransform([...secretsToRedact]);
627
- const stderrRedact = new RedactionTransform([...secretsToRedact]);
628
- if (child.stdout) child.stdout.pipe(stdoutRedact);
629
- if (child.stderr) child.stderr.pipe(stderrRedact);
630
- let stdoutStr = "";
631
- let stderrStr = "";
632
- if (opts.captureOutput) {
633
- stdoutRedact.on("data", (d) => stdoutStr += d.toString());
634
- stderrRedact.on("data", (d) => stderrStr += d.toString());
635
- } else {
636
- stdoutRedact.pipe(process.stdout);
637
- stderrRedact.pipe(process.stderr);
381
+ if (cmd.filter) {
382
+ entries = filterSecretsByKeyGlob(entries, cmd.filter);
638
383
  }
639
- child.on("close", (code) => {
640
- if (timer) clearTimeout(timer);
641
- if (timedOut) {
642
- resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
643
- [QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
644
- } else {
645
- resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
646
- }
647
- });
648
- child.on("error", (err) => {
649
- if (timer) clearTimeout(timer);
650
- reject(err);
651
- });
652
- });
653
- }
654
-
655
- // src/core/scan.ts
656
- import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
657
- import { join } from "path";
658
- var IGNORE_DIRS = /* @__PURE__ */ new Set([
659
- "node_modules",
660
- ".git",
661
- ".next",
662
- "dist",
663
- "build",
664
- "coverage",
665
- ".cursor",
666
- "venv",
667
- "__pycache__"
668
- ]);
669
- var IGNORE_EXTS = /* @__PURE__ */ new Set([
670
- ".png",
671
- ".jpg",
672
- ".jpeg",
673
- ".gif",
674
- ".ico",
675
- ".svg",
676
- ".webp",
677
- ".mp4",
678
- ".mp3",
679
- ".wav",
680
- ".ogg",
681
- ".pdf",
682
- ".zip",
683
- ".tar",
684
- ".gz",
685
- ".xz",
686
- ".ttf",
687
- ".woff",
688
- ".woff2",
689
- ".eot",
690
- ".exe",
691
- ".dll",
692
- ".so",
693
- ".dylib",
694
- ".lock"
695
- ]);
696
- var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
697
- function calculateEntropy(str) {
698
- if (!str) return 0;
699
- const len = str.length;
700
- const frequencies = /* @__PURE__ */ new Map();
701
- for (let i = 0; i < len; i++) {
702
- const char = str[i];
703
- frequencies.set(char, (frequencies.get(char) || 0) + 1);
704
- }
705
- let entropy = 0;
706
- for (const count of frequencies.values()) {
707
- const p = count / len;
708
- entropy -= p * Math.log2(p);
709
- }
710
- return entropy;
711
- }
712
- function scanCodebase(dir) {
713
- const results = [];
714
- function walk(currentDir) {
715
- let entries;
716
- try {
717
- entries = readdirSync(currentDir);
718
- } catch {
384
+ if (entries.length === 0) {
385
+ console.log(c.dim("No secrets found"));
719
386
  return;
720
387
  }
388
+ console.log(
389
+ c.bold(`
390
+ ${SYMBOLS.key} q-ring secrets (${entries.length})
391
+ `)
392
+ );
393
+ const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
721
394
  for (const entry of entries) {
722
- if (IGNORE_DIRS.has(entry)) continue;
723
- const fullPath = join(currentDir, entry);
724
- let stat;
725
- try {
726
- stat = statSync(fullPath);
727
- } catch {
728
- continue;
395
+ const parts = [];
396
+ const key = safeStr(entry.key);
397
+ const scope = safeStr(entry.scope);
398
+ const envs = entry.envelope?.states ? Object.keys(entry.envelope.states).map(safeStr) : null;
399
+ const entangledCount = safeNum(entry.envelope?.meta.entangled?.length);
400
+ const accessCount = safeNum(entry.envelope?.meta.accessCount);
401
+ const tags = safeArr(entry.envelope?.meta.tags);
402
+ const decayPct = safeNum(entry.decay?.lifetimePercent);
403
+ const expired = !!entry.decay?.isExpired;
404
+ const timeLeft = safeStr(entry.decay?.timeRemaining);
405
+ parts.push(c.dim("[") + scopeColor(scope) + c.dim("]"));
406
+ parts.push(c.bold(key.padEnd(maxKeyLen)));
407
+ if (envs) {
408
+ parts.push(c.magenta(`[${envs.join("|")}]`));
729
409
  }
730
- if (stat.isDirectory()) {
731
- walk(fullPath);
732
- } else if (stat.isFile()) {
733
- const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
734
- if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
735
- let content;
736
- try {
737
- content = readFileSync2(fullPath, "utf8");
738
- } catch {
739
- continue;
740
- }
741
- if (content.includes("\0")) continue;
742
- const lines = content.split(/\r?\n/);
743
- for (let i = 0; i < lines.length; i++) {
744
- const line = lines[i];
745
- if (line.length > 500) continue;
746
- const match = line.match(SECRET_KEYWORDS);
747
- if (match) {
748
- const varName = match[1];
749
- const value = match[3];
750
- if (value.length < 8) continue;
751
- const lowerValue = value.toLowerCase();
752
- if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
753
- continue;
754
- }
755
- const entropy = calculateEntropy(value);
756
- if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
757
- const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
758
- results.push({
759
- file: relPath || fullPath,
760
- line: i + 1,
761
- keyName: varName,
762
- match: value,
763
- context: line.trim(),
764
- entropy: parseFloat(entropy.toFixed(2))
765
- });
766
- }
767
- }
410
+ if (entry.decay && (decayPct > 0 || expired)) {
411
+ parts.push(decayIndicator(decayPct, expired));
412
+ if (timeLeft && !expired) {
413
+ parts.push(c.dim(timeLeft));
768
414
  }
769
415
  }
770
- }
771
- }
772
- walk(dir);
773
- return results;
774
- }
775
-
776
- // src/core/linter.ts
777
- import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
778
- import { basename, extname } from "path";
779
- var ENV_REF_BY_EXT = {
780
- ".ts": (k) => `process.env.${k}`,
781
- ".tsx": (k) => `process.env.${k}`,
782
- ".js": (k) => `process.env.${k}`,
783
- ".jsx": (k) => `process.env.${k}`,
784
- ".mjs": (k) => `process.env.${k}`,
785
- ".cjs": (k) => `process.env.${k}`,
786
- ".py": (k) => `os.environ["${k}"]`,
787
- ".rb": (k) => `ENV["${k}"]`,
788
- ".go": (k) => `os.Getenv("${k}")`,
789
- ".rs": (k) => `std::env::var("${k}")`,
790
- ".java": (k) => `System.getenv("${k}")`,
791
- ".kt": (k) => `System.getenv("${k}")`,
792
- ".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
793
- ".php": (k) => `getenv('${k}')`,
794
- ".sh": (k) => `\${${k}}`,
795
- ".bash": (k) => `\${${k}}`
796
- };
797
- function getEnvRef(filePath, keyName) {
798
- const ext = extname(filePath).toLowerCase();
799
- const formatter = ENV_REF_BY_EXT[ext];
800
- return formatter ? formatter(keyName) : `process.env.${keyName}`;
801
- }
802
- function lintFiles(files, opts = {}) {
803
- const results = [];
804
- for (const file of files) {
805
- if (!existsSync(file)) continue;
806
- let content;
807
- try {
808
- content = readFileSync3(file, "utf8");
809
- } catch {
810
- continue;
811
- }
812
- if (content.includes("\0")) continue;
813
- const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
814
- const lines = content.split(/\r?\n/);
815
- const fixes = [];
816
- for (let i = 0; i < lines.length; i++) {
817
- const line = lines[i];
818
- if (line.length > 500) continue;
819
- let match;
820
- SECRET_KEYWORDS2.lastIndex = 0;
821
- while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
822
- const varName = match[1].toUpperCase();
823
- const quote = match[2];
824
- const value = match[3];
825
- if (value.length < 8) continue;
826
- const lv = value.toLowerCase();
827
- if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
828
- const entropy = calculateEntropy2(value);
829
- if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
830
- const shouldFix = opts.fix === true;
831
- if (shouldFix) {
832
- const envRef = getEnvRef(file, varName);
833
- fixes.push({
834
- line: i,
835
- original: `${quote}${value}${quote}`,
836
- replacement: envRef,
837
- keyName: varName,
838
- value
839
- });
840
- }
841
- results.push({
842
- file,
843
- line: i + 1,
844
- keyName: varName,
845
- match: value,
846
- context: line.trim(),
847
- entropy: parseFloat(entropy.toFixed(2)),
848
- fixed: shouldFix
849
- });
416
+ if (entangledCount > 0) {
417
+ parts.push(c.cyan(`${SYMBOLS.link} ${entangledCount}`));
850
418
  }
851
- }
852
- if (opts.fix && fixes.length > 0) {
853
- const fixLines = content.split(/\r?\n/);
854
- for (const fix of fixes.reverse()) {
855
- const lineIdx = fix.line;
856
- if (lineIdx >= 0 && lineIdx < fixLines.length) {
857
- fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
858
- }
859
- if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
860
- setSecret(fix.keyName, fix.value, {
861
- scope: opts.scope ?? "global",
862
- projectPath: opts.projectPath,
863
- source: "cli",
864
- description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
865
- });
866
- }
419
+ if (accessCount > 0) {
420
+ parts.push(c.dim(`${SYMBOLS.eye} ${accessCount}`));
421
+ }
422
+ if (tags.length > 0) {
423
+ parts.push(c.dim(tags.map((t) => `#${t}`).join(" ")));
867
424
  }
868
- writeFileSync(file, fixLines.join("\n"), "utf8");
425
+ console.log(` ${parts.join(" ")}`);
869
426
  }
870
- }
871
- return results;
872
- }
873
- function calculateEntropy2(str) {
874
- if (!str) return 0;
875
- const len = str.length;
876
- const frequencies = /* @__PURE__ */ new Map();
877
- for (let i = 0; i < len; i++) {
878
- const ch = str[i];
879
- frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
880
- }
881
- let entropy = 0;
882
- for (const count of frequencies.values()) {
883
- const p = count / len;
884
- entropy -= p * Math.log2(p);
885
- }
886
- return entropy;
887
- }
888
-
889
- // src/core/validate.ts
890
- function makeRequest(url, headers, timeoutMs = 1e4) {
891
- return httpRequest_({ url, method: "GET", headers, timeoutMs });
892
- }
893
- var ProviderRegistry = class {
894
- providers = /* @__PURE__ */ new Map();
895
- register(provider) {
896
- this.providers.set(provider.name, provider);
897
- }
898
- get(name) {
899
- return this.providers.get(name);
900
- }
901
- detectProvider(value, hints) {
902
- if (hints?.provider) {
903
- return this.providers.get(hints.provider);
427
+ console.log();
428
+ });
429
+ program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
430
+ const opts = buildOpts(cmd);
431
+ const result = getEnvelope(key, opts);
432
+ if (!result) {
433
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
434
+ process.exit(1);
904
435
  }
905
- for (const provider of this.providers.values()) {
906
- if (provider.prefixes) {
907
- for (const pfx of provider.prefixes) {
908
- if (value.startsWith(pfx)) return provider;
909
- }
436
+ const { envelope, scope } = result;
437
+ const decay = checkDecay(envelope);
438
+ const safeScope = safeStr(scope);
439
+ const createdAt = safeStr(envelope.meta.createdAt);
440
+ const updatedAt = safeStr(envelope.meta.updatedAt);
441
+ const accessCount = safeNum(envelope.meta.accessCount);
442
+ const lastAccess = safeStr(envelope.meta.lastAccessedAt);
443
+ const desc = safeStr(envelope.meta.description);
444
+ const tags = safeArr(envelope.meta.tags);
445
+ const entangled = (envelope.meta.entangled ?? []).map((l) => ({
446
+ service: safeStr(l.service),
447
+ key: safeStr(l.key)
448
+ }));
449
+ const stateEnvs = envelope.states ? Object.keys(envelope.states).map(safeStr) : null;
450
+ const defaultEnv = safeStr(envelope.defaultEnv);
451
+ const decayTime = safeStr(decay.timeRemaining);
452
+ const decayPct = safeNum(decay.lifetimePercent);
453
+ const expired = !!decay.isExpired;
454
+ console.log(`
455
+ ${c.bold(SYMBOLS.key + " " + key)}`);
456
+ console.log(` ${c.dim("scope:")} ${scopeColor(safeScope)}`);
457
+ if (stateEnvs) {
458
+ console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
459
+ console.log(` ${c.dim("states:")}`);
460
+ for (const env of stateEnvs) {
461
+ const isDefault = env === defaultEnv;
462
+ console.log(
463
+ ` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
464
+ );
910
465
  }
466
+ } else {
467
+ console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
911
468
  }
912
- return void 0;
913
- }
914
- listProviders() {
915
- return [...this.providers.values()];
916
- }
917
- };
918
- var openaiProvider = {
919
- name: "openai",
920
- description: "OpenAI API key validation",
921
- prefixes: ["sk-"],
922
- async validate(value) {
923
- const start = Date.now();
924
- try {
925
- const { statusCode } = await makeRequest(
926
- "https://api.openai.com/v1/models?limit=1",
927
- {
928
- Authorization: `Bearer ${value}`,
929
- "User-Agent": "q-ring-validator/1.0"
930
- }
931
- );
932
- const latencyMs = Date.now() - start;
933
- if (statusCode === 200)
934
- return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
935
- if (statusCode === 401)
936
- return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
937
- if (statusCode === 429)
938
- return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
939
- return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
940
- } catch (err) {
941
- return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
469
+ console.log(` ${c.dim("created:")} ${createdAt}`);
470
+ console.log(` ${c.dim("updated:")} ${updatedAt}`);
471
+ console.log(` ${c.dim("accessed:")} ${accessCount} times`);
472
+ if (lastAccess) {
473
+ console.log(` ${c.dim("last read:")} ${lastAccess}`);
942
474
  }
943
- }
944
- };
945
- var stripeProvider = {
946
- name: "stripe",
947
- description: "Stripe API key validation",
948
- prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
949
- async validate(value) {
950
- const start = Date.now();
951
- try {
952
- const { statusCode } = await makeRequest(
953
- "https://api.stripe.com/v1/balance",
954
- {
955
- Authorization: `Bearer ${value}`,
956
- "User-Agent": "q-ring-validator/1.0"
957
- }
475
+ if (desc) {
476
+ console.log(` ${c.dim("desc:")} ${desc}`);
477
+ }
478
+ if (tags.length > 0) {
479
+ console.log(
480
+ ` ${c.dim("tags:")} ${tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
958
481
  );
959
- const latencyMs = Date.now() - start;
960
- if (statusCode === 200)
961
- return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
962
- if (statusCode === 401)
963
- return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
964
- if (statusCode === 429)
965
- return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
966
- return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
967
- } catch (err) {
968
- return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
969
482
  }
970
- }
483
+ if (decayTime) {
484
+ console.log(
485
+ ` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
486
+ );
487
+ }
488
+ if (entangled.length > 0) {
489
+ console.log(` ${c.dim("entangled:")}`);
490
+ for (const link of entangled) {
491
+ console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
492
+ }
493
+ }
494
+ console.log();
495
+ });
496
+ 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) => {
497
+ const opts = buildOpts(cmd);
498
+ const output = exportSecrets({
499
+ ...opts,
500
+ format: cmd.format,
501
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
502
+ tags: cmd.tags?.split(",").map((t) => t.trim())
503
+ });
504
+ process.stdout.write(output + "\n");
505
+ });
506
+ 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) => {
507
+ const opts = buildOpts(cmd);
508
+ const result = importDotenv(file, {
509
+ scope: opts.scope,
510
+ projectPath: opts.projectPath,
511
+ source: "cli",
512
+ skipExisting: cmd.skipExisting,
513
+ dryRun: cmd.dryRun
514
+ });
515
+ if (cmd.dryRun) {
516
+ console.log(
517
+ `
518
+ ${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
519
+ `
520
+ );
521
+ for (const key of result.imported) {
522
+ console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
523
+ }
524
+ if (result.skipped.length > 0) {
525
+ console.log(
526
+ `
527
+ ${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`
528
+ );
529
+ }
530
+ } else {
531
+ console.log(
532
+ `${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
533
+ );
534
+ if (result.skipped.length > 0) {
535
+ console.log(
536
+ c.dim(
537
+ ` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`
538
+ )
539
+ );
540
+ }
541
+ }
542
+ console.log();
543
+ });
544
+ }
545
+
546
+ // src/core/noise.ts
547
+ import { randomBytes, randomInt } from "crypto";
548
+ var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
549
+ var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
550
+ function randomString(charset, length) {
551
+ let result = "";
552
+ for (let i = 0; i < length; i++) {
553
+ result += charset[randomInt(charset.length)];
554
+ }
555
+ return result;
556
+ }
557
+ function generateSecret(opts = {}) {
558
+ const format = opts.format ?? "api-key";
559
+ switch (format) {
560
+ case "hex": {
561
+ const len = opts.length ?? 32;
562
+ return randomBytes(len).toString("hex");
563
+ }
564
+ case "base64": {
565
+ const len = opts.length ?? 32;
566
+ return randomBytes(len).toString("base64url");
567
+ }
568
+ case "alphanumeric": {
569
+ const len = opts.length ?? 32;
570
+ return randomString(ALPHA_NUM, len);
571
+ }
572
+ case "uuid": {
573
+ const bytes = randomBytes(16);
574
+ bytes[6] = bytes[6] & 15 | 64;
575
+ bytes[8] = bytes[8] & 63 | 128;
576
+ const hex = bytes.toString("hex");
577
+ return [
578
+ hex.slice(0, 8),
579
+ hex.slice(8, 12),
580
+ hex.slice(12, 16),
581
+ hex.slice(16, 20),
582
+ hex.slice(20, 32)
583
+ ].join("-");
584
+ }
585
+ case "api-key": {
586
+ const prefix = opts.prefix ?? "qr_";
587
+ const len = opts.length ?? 48;
588
+ return prefix + randomString(ALPHA_NUM, len);
589
+ }
590
+ case "token": {
591
+ const prefix = opts.prefix ?? "";
592
+ const len = opts.length ?? 64;
593
+ return prefix + randomBytes(len).toString("base64url");
594
+ }
595
+ case "password": {
596
+ const len = opts.length ?? 24;
597
+ let pw = randomString(PASSWORD_CHARS, len);
598
+ const hasUpper = /[A-Z]/.test(pw);
599
+ const hasLower = /[a-z]/.test(pw);
600
+ const hasDigit = /[0-9]/.test(pw);
601
+ const hasSpecial = /[^A-Za-z0-9]/.test(pw);
602
+ if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
603
+ if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
604
+ if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
605
+ if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
606
+ return pw;
607
+ }
608
+ default:
609
+ return randomBytes(32).toString("hex");
610
+ }
611
+ }
612
+ function replaceAt(str, index, char) {
613
+ return str.slice(0, index) + char + str.slice(index + 1);
614
+ }
615
+ function estimateEntropy(secret) {
616
+ const charsets = [
617
+ { regex: /[a-z]/, size: 26 },
618
+ { regex: /[A-Z]/, size: 26 },
619
+ { regex: /[0-9]/, size: 10 },
620
+ { regex: /[^A-Za-z0-9]/, size: 32 }
621
+ ];
622
+ let poolSize = 0;
623
+ for (const { regex, size } of charsets) {
624
+ if (regex.test(secret)) poolSize += size;
625
+ }
626
+ return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
627
+ }
628
+
629
+ // src/core/validate.ts
630
+ function makeRequest(url, headers, timeoutMs = 1e4) {
631
+ return httpRequest({ url, method: "GET", headers, timeoutMs });
632
+ }
633
+ var ProviderRegistry = class {
634
+ providers = /* @__PURE__ */ new Map();
635
+ register(provider) {
636
+ this.providers.set(provider.name, provider);
637
+ }
638
+ get(name) {
639
+ return this.providers.get(name);
640
+ }
641
+ detectProvider(value, hints) {
642
+ if (hints?.provider) {
643
+ return this.providers.get(hints.provider);
644
+ }
645
+ for (const provider of this.providers.values()) {
646
+ if (provider.prefixes) {
647
+ for (const pfx of provider.prefixes) {
648
+ if (value.startsWith(pfx)) return provider;
649
+ }
650
+ }
651
+ }
652
+ return void 0;
653
+ }
654
+ listProviders() {
655
+ return [...this.providers.values()];
656
+ }
657
+ };
658
+ var openaiProvider = {
659
+ name: "openai",
660
+ description: "OpenAI API key validation",
661
+ prefixes: ["sk-"],
662
+ async validate(value) {
663
+ const start = Date.now();
664
+ try {
665
+ const { statusCode } = await makeRequest(
666
+ "https://api.openai.com/v1/models?limit=1",
667
+ {
668
+ Authorization: `Bearer ${value}`,
669
+ "User-Agent": "q-ring-validator/1.0"
670
+ }
671
+ );
672
+ const latencyMs = Date.now() - start;
673
+ if (statusCode === 200)
674
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
675
+ if (statusCode === 401)
676
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
677
+ if (statusCode === 429)
678
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
679
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
680
+ } catch (err) {
681
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
682
+ }
683
+ }
684
+ };
685
+ var stripeProvider = {
686
+ name: "stripe",
687
+ description: "Stripe API key validation",
688
+ prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
689
+ async validate(value) {
690
+ const start = Date.now();
691
+ try {
692
+ const { statusCode } = await makeRequest(
693
+ "https://api.stripe.com/v1/balance",
694
+ {
695
+ Authorization: `Bearer ${value}`,
696
+ "User-Agent": "q-ring-validator/1.0"
697
+ }
698
+ );
699
+ const latencyMs = Date.now() - start;
700
+ if (statusCode === 200)
701
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
702
+ if (statusCode === 401)
703
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
704
+ if (statusCode === 429)
705
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
706
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
707
+ } catch (err) {
708
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
709
+ }
710
+ }
971
711
  };
972
712
  var githubProvider = {
973
713
  name: "github",
@@ -1163,522 +903,93 @@ function getProjectContext(opts = {}) {
1163
903
  };
1164
904
  }
1165
905
 
1166
- // src/core/memory.ts
1167
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
1168
- import { join as join2 } from "path";
1169
- import { homedir, hostname, userInfo } from "os";
1170
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
1171
- import { Entry } from "@napi-rs/keyring";
1172
- var MEMORY_FILE = "agent-memory.enc";
1173
- var KEYRING_SERVICE = "qring-memory-key";
1174
- var KEYRING_ACCOUNT = "encryption-key";
1175
- function getMemoryDir() {
1176
- const dir = join2(homedir(), ".config", "q-ring");
1177
- if (!existsSync2(dir)) {
1178
- mkdirSync(dir, { recursive: true });
1179
- }
1180
- return dir;
1181
- }
1182
- function getMemoryPath() {
1183
- return join2(getMemoryDir(), MEMORY_FILE);
1184
- }
1185
- function deriveLegacyKey() {
1186
- const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1187
- return createHash("sha256").update(fingerprint).digest();
1188
- }
1189
- function getOrCreateKey() {
1190
- try {
1191
- const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
1192
- const stored = entry.getPassword();
1193
- if (stored) return Buffer.from(stored, "base64");
1194
- const key = randomBytes3(32);
1195
- entry.setPassword(key.toString("base64"));
1196
- return key;
1197
- } catch {
1198
- console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
1199
- return deriveLegacyKey();
1200
- }
1201
- }
1202
- function encryptWith(data, key) {
1203
- const iv = randomBytes3(12);
1204
- const cipher = createCipheriv2("aes-256-gcm", key, iv);
1205
- const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
1206
- const tag = cipher.getAuthTag();
1207
- return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
1208
- }
1209
- function decryptWith(blob, key) {
1210
- const parts = blob.split(":");
1211
- if (parts.length !== 3) throw new Error("Invalid encrypted format");
1212
- const iv = Buffer.from(parts[0], "base64");
1213
- const tag = Buffer.from(parts[1], "base64");
1214
- const encrypted = Buffer.from(parts[2], "base64");
1215
- const decipher = createDecipheriv2("aes-256-gcm", key, iv);
1216
- decipher.setAuthTag(tag);
1217
- return decipher.update(encrypted) + decipher.final("utf8");
1218
- }
1219
- function encrypt(data) {
1220
- return encryptWith(data, getOrCreateKey());
1221
- }
1222
- function decrypt(blob) {
1223
- const key = getOrCreateKey();
1224
- try {
1225
- return decryptWith(blob, key);
1226
- } catch {
1227
- const legacy = deriveLegacyKey();
1228
- const plain = decryptWith(blob, legacy);
1229
- writeFileSync2(getMemoryPath(), encryptWith(plain, key), "utf8");
1230
- return plain;
1231
- }
1232
- }
1233
- function loadStore() {
1234
- const path = getMemoryPath();
1235
- if (!existsSync2(path)) {
1236
- return { entries: {} };
1237
- }
1238
- try {
1239
- const raw = readFileSync4(path, "utf8");
1240
- const decrypted = decrypt(raw);
1241
- return JSON.parse(decrypted);
1242
- } catch {
1243
- return { entries: {} };
1244
- }
1245
- }
1246
- function saveStore(store) {
1247
- const json = JSON.stringify(store);
1248
- const encrypted = encrypt(json);
1249
- writeFileSync2(getMemoryPath(), encrypted, "utf8");
1250
- }
1251
- function remember(key, value) {
1252
- const store = loadStore();
1253
- store.entries[key] = {
1254
- value,
1255
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1256
- };
1257
- saveStore(store);
1258
- }
1259
- function recall(key) {
1260
- const store = loadStore();
1261
- return store.entries[key]?.value ?? null;
1262
- }
1263
- function listMemory() {
1264
- const store = loadStore();
1265
- return Object.entries(store.entries).map(([key, entry]) => ({
1266
- key,
1267
- updatedAt: entry.updatedAt
1268
- }));
1269
- }
1270
- function forget(key) {
1271
- const store = loadStore();
1272
- if (key in store.entries) {
1273
- delete store.entries[key];
1274
- saveStore(store);
1275
- return true;
1276
- }
1277
- return false;
1278
- }
1279
- function clearMemory() {
1280
- saveStore({ entries: {} });
1281
- }
1282
-
1283
- // src/hooks/precommit.ts
1284
- import { execSync } from "child_process";
1285
- import { existsSync as existsSync3, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
1286
- import { join as join3 } from "path";
1287
- function getStagedFiles() {
1288
- try {
1289
- const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
1290
- encoding: "utf8",
1291
- stdio: ["pipe", "pipe", "pipe"]
1292
- });
1293
- return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
1294
- } catch {
1295
- return [];
1296
- }
1297
- }
1298
- function runPreCommitScan() {
1299
- const staged = getStagedFiles();
1300
- if (staged.length === 0) return 0;
1301
- const results = lintFiles(staged);
1302
- if (results.length === 0) return 0;
1303
- console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
1304
- for (const r of results) {
1305
- console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
1306
- }
1307
- console.error(
1308
- `
1309
- [q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
1310
- `
1311
- );
1312
- return 1;
1313
- }
1314
- var HOOK_SCRIPT = `#!/bin/sh
1315
- # q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
1316
- npx qring hook:run 2>&1
1317
- exit $?
1318
- `;
1319
- function installPreCommitHook(repoPath) {
1320
- const root = repoPath ?? process.cwd();
1321
- const hooksDir = join3(root, ".git", "hooks");
1322
- if (!existsSync3(join3(root, ".git"))) {
1323
- return { installed: false, path: "", message: "Not a git repository" };
1324
- }
1325
- const hookPath = join3(hooksDir, "pre-commit");
1326
- if (existsSync3(hookPath)) {
1327
- const existing = readFileSync5(hookPath, "utf8");
1328
- if (existing.includes("q-ring")) {
1329
- return { installed: true, path: hookPath, message: "Hook already installed" };
1330
- }
1331
- writeFileSync3(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
1332
- } else {
1333
- writeFileSync3(hookPath, HOOK_SCRIPT, "utf8");
1334
- }
1335
- chmodSync(hookPath, 493);
1336
- return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
1337
- }
1338
- function uninstallPreCommitHook(repoPath) {
1339
- const root = repoPath ?? process.cwd();
1340
- const hookPath = join3(root, ".git", "hooks", "pre-commit");
1341
- if (!existsSync3(hookPath)) return false;
1342
- const content = readFileSync5(hookPath, "utf8");
1343
- if (!content.includes("q-ring")) return false;
1344
- const lines = content.split("\n");
1345
- const cleaned = lines.filter(
1346
- (l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
1347
- );
1348
- if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
1349
- unlinkSync(hookPath);
1350
- } else {
1351
- writeFileSync3(hookPath, cleaned.join("\n"), "utf8");
1352
- }
1353
- return true;
1354
- }
1355
-
1356
- // src/cli/commands.ts
1357
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1358
-
1359
- // src/utils/prompt.ts
1360
- import { createInterface } from "readline";
1361
- async function promptSecret(message) {
1362
- const rl = createInterface({
1363
- input: process.stdin,
1364
- output: process.stderr,
1365
- terminal: true
1366
- });
1367
- return new Promise((resolve) => {
1368
- process.stderr.write(message);
1369
- const stdin = process.stdin;
1370
- const wasRaw = stdin.isRaw;
1371
- if (stdin.isTTY && stdin.setRawMode) {
1372
- stdin.setRawMode(true);
1373
- }
1374
- let input = "";
1375
- const onData = (chunk) => {
1376
- const char = chunk.toString("utf8");
1377
- if (char === "\n" || char === "\r") {
1378
- stdin.removeListener("data", onData);
1379
- if (stdin.isTTY && stdin.setRawMode) {
1380
- stdin.setRawMode(wasRaw ?? false);
1381
- }
1382
- process.stderr.write("\n");
1383
- rl.close();
1384
- resolve(input);
1385
- } else if (char === "") {
1386
- process.exit(1);
1387
- } else if (char === "\x7F" || char === "\b") {
1388
- input = input.slice(0, -1);
1389
- } else {
1390
- input += char;
1391
- }
1392
- };
1393
- stdin.on("data", onData);
1394
- });
1395
- }
1396
-
1397
- // src/cli/commands.ts
1398
- function safeStr(s) {
1399
- return s == null ? "" : `${s}`;
1400
- }
1401
- function safeNum(n) {
1402
- return n == null ? 0 : Number(n);
1403
- }
1404
- function safeArr(arr) {
1405
- return arr ? arr.map((x) => typeof x === "string" ? safeStr(x) : x) : [];
1406
- }
1407
- function buildOpts(cmd) {
1408
- let scope;
1409
- if (cmd.global) scope = "global";
1410
- else if (cmd.project) scope = "project";
1411
- else if (cmd.team) scope = "team";
1412
- else if (cmd.org) scope = "org";
1413
- const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
1414
- if (scope === "project" && !projectPath) {
1415
- throw new Error("Project path is required for project scope");
1416
- }
1417
- return {
1418
- scope,
1419
- projectPath: projectPath ?? process.cwd(),
1420
- teamId: cmd.team,
1421
- orgId: cmd.org,
1422
- env: cmd.env,
1423
- source: "cli"
1424
- };
1425
- }
1426
- function createProgram() {
1427
- const program2 = new Command().name("qring").description(
1428
- `${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
1429
- ).version("0.4.0");
1430
- 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) => {
906
+ // src/cli/commands/project.ts
907
+ import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
908
+ function registerProjectCommands(program2) {
909
+ program2.command("context").alias("describe").description(
910
+ "Show safe, redacted project context for AI agents (no secret values exposed)"
911
+ ).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) => {
1431
912
  const opts = buildOpts(cmd);
1432
- if (!value) {
1433
- value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
1434
- if (!value) {
1435
- console.error(c.red(`${SYMBOLS.cross} No value provided, aborting.`));
1436
- process.exit(1);
1437
- }
913
+ const context = getProjectContext(opts);
914
+ if (wantsJsonOutput(program2, cmd)) {
915
+ console.log(JSON.stringify(context, null, 2));
916
+ return;
1438
917
  }
1439
- const setOpts = {
1440
- ...opts,
1441
- ttlSeconds: cmd.ttl,
1442
- expiresAt: cmd.expires,
1443
- description: cmd.description,
1444
- tags: cmd.tags?.split(",").map((t) => t.trim()),
1445
- rotationFormat: cmd.rotationFormat,
1446
- rotationPrefix: cmd.rotationPrefix,
1447
- requiresApproval: cmd.requiresApproval,
1448
- jitProvider: cmd.jitProvider
1449
- };
1450
- if (cmd.env) {
1451
- const existing = getEnvelope(key, opts);
1452
- const states = existing?.envelope?.states ?? {};
1453
- states[cmd.env] = value;
1454
- if (existing?.envelope?.value && !states["default"]) {
1455
- states["default"] = existing.envelope.value;
1456
- }
1457
- setOpts.states = states;
1458
- setOpts.defaultEnv = existing?.envelope?.defaultEnv ?? cmd.env;
1459
- setSecret(key, "", setOpts);
918
+ console.log(`
919
+ ${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
920
+ console.log(` Project: ${c.cyan(context.projectPath)}`);
921
+ if (context.environment) {
1460
922
  console.log(
1461
- `${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${envBadge(cmd.env)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)}`
923
+ ` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`
1462
924
  );
1463
- } else {
1464
- setSecret(key, value, setOpts);
1465
- const extras = [];
1466
- if (cmd.ttl) extras.push(`${SYMBOLS.clock} ttl=${cmd.ttl}s`);
1467
- if (cmd.description) extras.push(c.dim(cmd.description));
925
+ }
926
+ console.log(
927
+ `
928
+ ${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`
929
+ );
930
+ for (const s of context.secrets) {
931
+ const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
932
+ const flags = [];
933
+ if (s.requiresApproval) flags.push(c.yellow("locked"));
934
+ if (s.jitProvider) flags.push(c.magenta("jit"));
935
+ if (s.hasStates) flags.push(c.blue("superposition"));
936
+ if (s.isExpired) flags.push(c.red("expired"));
937
+ else if (s.isStale) flags.push(c.yellow("stale"));
938
+ const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
1468
939
  console.log(
1469
- `${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)} ${extras.join(" ")}`
940
+ ` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`
1470
941
  );
1471
942
  }
1472
- });
1473
- 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) => {
1474
- const opts = buildOpts(cmd);
1475
- const value = getSecret(key, opts);
1476
- if (value === null) {
1477
- console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1478
- process.exit(1);
1479
- }
1480
- process.stdout.write(value);
1481
- });
1482
- program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
1483
- const opts = buildOpts(cmd);
1484
- const deleted = deleteSecret(key, opts);
1485
- if (deleted) {
1486
- console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
1487
- } else {
1488
- console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1489
- process.exit(1);
1490
- }
1491
- });
1492
- 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) => {
1493
- const opts = buildOpts(cmd);
1494
- let entries = listSecrets(opts);
1495
- if (cmd.tag) {
1496
- entries = entries.filter(
1497
- (e) => e.envelope?.meta.tags?.includes(cmd.tag)
943
+ if (context.manifest) {
944
+ console.log(
945
+ `
946
+ ${c.bold(" Manifest:")} ${context.manifest.declared} declared`
1498
947
  );
948
+ if (context.manifest.missing.length > 0) {
949
+ console.log(
950
+ ` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`
951
+ );
952
+ } else {
953
+ console.log(
954
+ ` ${c.green(SYMBOLS.check)} All manifest secrets present`
955
+ );
956
+ }
1499
957
  }
1500
- if (cmd.expired) {
1501
- entries = entries.filter((e) => e.decay?.isExpired);
1502
- }
1503
- if (cmd.stale) {
1504
- entries = entries.filter(
1505
- (e) => e.decay?.isStale && !e.decay?.isExpired
958
+ console.log(
959
+ `
960
+ ${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`
961
+ );
962
+ console.log(
963
+ `${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`
964
+ );
965
+ console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
966
+ if (context.recentActions.length > 0) {
967
+ console.log(
968
+ `
969
+ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1506
970
  );
971
+ for (const a of context.recentActions.slice(0, 8)) {
972
+ const ts = new Date(a.timestamp).toLocaleTimeString();
973
+ console.log(
974
+ ` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`
975
+ );
976
+ }
1507
977
  }
1508
- if (cmd.filter) {
1509
- const regex = new RegExp(
1510
- "^" + cmd.filter.replace(/\*/g, ".*") + "$",
1511
- "i"
978
+ console.log();
979
+ });
980
+ program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
981
+ const projectPath = cmd.projectPath ?? process.cwd();
982
+ const config = readProjectConfig(projectPath);
983
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
984
+ console.error(
985
+ c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
986
+ );
987
+ console.log(
988
+ c.dim(
989
+ ' Add a "secrets" field to your .q-ring.json to define required secrets.'
990
+ )
1512
991
  );
1513
- entries = entries.filter((e) => regex.test(e.key));
1514
- }
1515
- if (entries.length === 0) {
1516
- console.log(c.dim("No secrets found"));
1517
- return;
1518
- }
1519
- console.log(
1520
- c.bold(`
1521
- ${SYMBOLS.key} q-ring secrets (${entries.length})
1522
- `)
1523
- );
1524
- const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
1525
- for (const entry of entries) {
1526
- const parts = [];
1527
- const key = safeStr(entry.key);
1528
- const scope = safeStr(entry.scope);
1529
- const envs = entry.envelope?.states ? Object.keys(entry.envelope.states).map(safeStr) : null;
1530
- const entangledCount = safeNum(entry.envelope?.meta.entangled?.length);
1531
- const accessCount = safeNum(entry.envelope?.meta.accessCount);
1532
- const tags = safeArr(entry.envelope?.meta.tags);
1533
- const decayPct = safeNum(entry.decay?.lifetimePercent);
1534
- const expired = !!entry.decay?.isExpired;
1535
- const timeLeft = safeStr(entry.decay?.timeRemaining);
1536
- parts.push(c.dim("[") + scopeColor(scope) + c.dim("]"));
1537
- parts.push(c.bold(key.padEnd(maxKeyLen)));
1538
- if (envs) {
1539
- parts.push(c.magenta(`[${envs.join("|")}]`));
1540
- }
1541
- if (entry.decay && (decayPct > 0 || expired)) {
1542
- parts.push(decayIndicator(decayPct, expired));
1543
- if (timeLeft && !expired) {
1544
- parts.push(c.dim(timeLeft));
1545
- }
1546
- }
1547
- if (entangledCount > 0) {
1548
- parts.push(c.cyan(`${SYMBOLS.link} ${entangledCount}`));
1549
- }
1550
- if (accessCount > 0) {
1551
- parts.push(c.dim(`${SYMBOLS.eye} ${accessCount}`));
1552
- }
1553
- if (tags.length > 0) {
1554
- parts.push(c.dim(tags.map((t) => `#${t}`).join(" ")));
1555
- }
1556
- console.log(` ${parts.join(" ")}`);
1557
- }
1558
- console.log();
1559
- });
1560
- program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
1561
- const opts = buildOpts(cmd);
1562
- const result = getEnvelope(key, opts);
1563
- if (!result) {
1564
- console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1565
- process.exit(1);
1566
- }
1567
- const { envelope, scope } = result;
1568
- const decay = checkDecay(envelope);
1569
- const safeScope = safeStr(scope);
1570
- const createdAt = safeStr(envelope.meta.createdAt);
1571
- const updatedAt = safeStr(envelope.meta.updatedAt);
1572
- const accessCount = safeNum(envelope.meta.accessCount);
1573
- const lastAccess = safeStr(envelope.meta.lastAccessedAt);
1574
- const desc = safeStr(envelope.meta.description);
1575
- const tags = safeArr(envelope.meta.tags);
1576
- const entangled = (envelope.meta.entangled ?? []).map((l) => ({
1577
- service: safeStr(l.service),
1578
- key: safeStr(l.key)
1579
- }));
1580
- const stateEnvs = envelope.states ? Object.keys(envelope.states).map(safeStr) : null;
1581
- const defaultEnv = safeStr(envelope.defaultEnv);
1582
- const decayTime = safeStr(decay.timeRemaining);
1583
- const decayPct = safeNum(decay.lifetimePercent);
1584
- const expired = !!decay.isExpired;
1585
- console.log(`
1586
- ${c.bold(SYMBOLS.key + " " + key)}`);
1587
- console.log(` ${c.dim("scope:")} ${scopeColor(safeScope)}`);
1588
- if (stateEnvs) {
1589
- console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
1590
- console.log(` ${c.dim("states:")}`);
1591
- for (const env of stateEnvs) {
1592
- const isDefault = env === defaultEnv;
1593
- console.log(
1594
- ` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
1595
- );
1596
- }
1597
- } else {
1598
- console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
1599
- }
1600
- console.log(` ${c.dim("created:")} ${createdAt}`);
1601
- console.log(` ${c.dim("updated:")} ${updatedAt}`);
1602
- console.log(` ${c.dim("accessed:")} ${accessCount} times`);
1603
- if (lastAccess) {
1604
- console.log(` ${c.dim("last read:")} ${lastAccess}`);
1605
- }
1606
- if (desc) {
1607
- console.log(` ${c.dim("desc:")} ${desc}`);
1608
- }
1609
- if (tags.length > 0) {
1610
- console.log(
1611
- ` ${c.dim("tags:")} ${tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
1612
- );
1613
- }
1614
- if (decayTime) {
1615
- console.log(
1616
- ` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
1617
- );
1618
- }
1619
- if (entangled.length > 0) {
1620
- console.log(` ${c.dim("entangled:")}`);
1621
- for (const link of entangled) {
1622
- console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
1623
- }
1624
- }
1625
- console.log();
1626
- });
1627
- 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) => {
1628
- const opts = buildOpts(cmd);
1629
- const output = exportSecrets({
1630
- ...opts,
1631
- format: cmd.format,
1632
- keys: cmd.keys?.split(",").map((k) => k.trim()),
1633
- tags: cmd.tags?.split(",").map((t) => t.trim())
1634
- });
1635
- process.stdout.write(output + "\n");
1636
- });
1637
- 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) => {
1638
- const opts = buildOpts(cmd);
1639
- const result = importDotenv(file, {
1640
- scope: opts.scope,
1641
- projectPath: opts.projectPath,
1642
- source: "cli",
1643
- skipExisting: cmd.skipExisting,
1644
- dryRun: cmd.dryRun
1645
- });
1646
- if (cmd.dryRun) {
1647
- console.log(
1648
- `
1649
- ${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
1650
- `
1651
- );
1652
- for (const key of result.imported) {
1653
- console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
1654
- }
1655
- if (result.skipped.length > 0) {
1656
- console.log(`
1657
- ${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
1658
- }
1659
- } else {
1660
- console.log(
1661
- `${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
1662
- );
1663
- if (result.skipped.length > 0) {
1664
- console.log(
1665
- c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
1666
- );
1667
- }
1668
- }
1669
- console.log();
1670
- });
1671
- program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
1672
- const projectPath = cmd.projectPath ?? process.cwd();
1673
- const config = readProjectConfig(projectPath);
1674
- if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1675
- console.error(
1676
- c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
1677
- );
1678
- console.log(
1679
- c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
1680
- );
1681
- process.exit(1);
992
+ process.exit(1);
1682
993
  }
1683
994
  console.log(
1684
995
  c.bold(`
@@ -1746,313 +1057,93 @@ function createProgram() {
1746
1057
  console.log();
1747
1058
  if (missing > 0) process.exit(1);
1748
1059
  });
1749
- 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) => {
1750
- if (cmd.listProviders) {
1751
- console.log(c.bold(`
1752
- ${SYMBOLS.shield} Available validation providers
1753
- `));
1754
- for (const p of registry2.listProviders()) {
1755
- const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
1756
- console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
1757
- }
1758
- console.log();
1759
- return;
1060
+ program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
1061
+ const result = collapseEnvironment({
1062
+ projectPath: cmd.projectPath ?? process.cwd()
1063
+ });
1064
+ if (result) {
1065
+ console.log(
1066
+ `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
1067
+ );
1068
+ } else {
1069
+ console.log(
1070
+ c.dim(
1071
+ "No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json"
1072
+ )
1073
+ );
1760
1074
  }
1761
- if (!key && !cmd.all) {
1762
- console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
1075
+ });
1076
+ program2.command("env:generate").description(
1077
+ "Generate a .env file from the project manifest (.q-ring.json)"
1078
+ ).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) => {
1079
+ const projectPath = cmd.projectPath ?? process.cwd();
1080
+ const config = readProjectConfig(projectPath);
1081
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1082
+ console.error(
1083
+ c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
1084
+ );
1763
1085
  process.exit(1);
1764
1086
  }
1765
1087
  const opts = buildOpts(cmd);
1766
- if (cmd.all) {
1767
- let entries = listSecrets(opts);
1768
- const projectPath = cmd.projectPath ?? process.cwd();
1769
- if (cmd.manifest) {
1770
- const config = readProjectConfig(projectPath);
1771
- if (config?.secrets) {
1772
- const manifestKeys = new Set(Object.keys(config.secrets));
1773
- entries = entries.filter((e) => manifestKeys.has(e.key));
1088
+ const lines = [];
1089
+ const warnings = [];
1090
+ for (const [key, manifest] of Object.entries(config.secrets)) {
1091
+ const value = getSecret(key, { ...opts, projectPath, source: "cli" });
1092
+ if (value === null) {
1093
+ if (manifest.required !== false) {
1094
+ warnings.push(`MISSING (required): ${key}`);
1774
1095
  }
1096
+ lines.push(
1097
+ `# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`
1098
+ );
1099
+ continue;
1775
1100
  }
1776
- console.log(c.bold(`
1777
- ${SYMBOLS.shield} Validating secrets
1778
- `));
1779
- let validated = 0;
1780
- let skipped = 0;
1781
- for (const entry of entries) {
1782
- const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
1783
- if (!value2) {
1784
- skipped++;
1785
- continue;
1786
- }
1787
- const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
1788
- const result2 = await validateSecret(value2, { provider: provHint2 });
1789
- if (result2.status === "unknown") {
1790
- skipped++;
1791
- continue;
1101
+ const result = getEnvelope(key, { projectPath, source: "cli" });
1102
+ if (result) {
1103
+ const decay = checkDecay(result.envelope);
1104
+ if (decay.isExpired) {
1105
+ warnings.push(`EXPIRED: ${key}`);
1106
+ } else if (decay.isStale) {
1107
+ warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
1792
1108
  }
1793
- validated++;
1794
- const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
1795
- const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
1796
- console.log(
1797
- ` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
1798
- );
1799
1109
  }
1800
- console.log(`
1801
- ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
1802
- `);
1803
- return;
1110
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1111
+ lines.push(`${key}="${escaped}"`);
1804
1112
  }
1805
- const value = getSecret(key, opts);
1806
- if (!value) {
1807
- console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1808
- process.exit(1);
1113
+ const output = lines.join("\n") + "\n";
1114
+ if (cmd.output) {
1115
+ writeFileSync(cmd.output, output);
1116
+ console.log(
1117
+ `${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
1118
+ );
1119
+ } else {
1120
+ process.stdout.write(output);
1809
1121
  }
1810
- const envelope = getEnvelope(key, opts);
1811
- const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
1812
- const result = await validateSecret(value, { provider: provHint });
1813
- 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");
1814
- console.log(`
1815
- ${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
1816
- if (result.message && result.status !== "valid") {
1817
- console.log(` ${c.dim(result.message)}`);
1818
- }
1819
- console.log();
1820
- });
1821
- 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) => {
1822
- const opts = buildOpts(cmd);
1823
- const command = commandArgs[0];
1824
- const args = commandArgs.slice(1);
1825
- try {
1826
- const { code } = await execCommand({
1827
- ...opts,
1828
- command,
1829
- args,
1830
- keys: cmd.keys?.split(",").map((k) => k.trim()),
1831
- tags: cmd.tags?.split(",").map((t) => t.trim()),
1832
- profile: cmd.profile
1833
- });
1834
- process.exit(code);
1835
- } catch (err) {
1836
- console.error(c.red(`${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`));
1837
- process.exit(1);
1838
- }
1839
- });
1840
- 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) => {
1841
- const targetDir = dir ?? process.cwd();
1842
- const fixMode = cmd.fix === true;
1843
- console.log(`
1844
- ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
1845
- `);
1846
- const results = scanCodebase(targetDir);
1847
- if (results.length === 0) {
1848
- console.log(` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
1849
- `);
1850
- return;
1851
- }
1852
- if (fixMode) {
1853
- const fileSet = new Set(results.map((r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`));
1854
- const opts = buildOpts(cmd);
1855
- const lintResults = lintFiles([...fileSet], { fix: true, scope: opts.scope, projectPath: opts.projectPath });
1856
- const fixedCount = lintResults.filter((r) => r.fixed).length;
1857
- console.log(` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
1858
- `);
1859
- return;
1860
- }
1861
- for (const res of results) {
1862
- console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
1863
- console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
1864
- console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
1865
- console.log(` ${c.dim("Context:")} ${res.context}`);
1866
- console.log();
1867
- }
1868
- console.log(` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
1869
- `);
1870
- });
1871
- 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) => {
1872
- const opts = buildOpts(cmd);
1873
- const results = lintFiles(files, { fix: cmd.fix, scope: opts.scope, projectPath: opts.projectPath });
1874
- if (results.length === 0) {
1875
- console.log(`
1876
- ${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
1877
- `);
1878
- return;
1879
- }
1880
- for (const res of results) {
1881
- const status = res.fixed ? c.green("fixed") : c.red("found");
1882
- console.log(` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`);
1883
- console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
1884
- console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
1885
- console.log();
1886
- }
1887
- const fixedCount = results.filter((r) => r.fixed).length;
1888
- if (cmd.fix && fixedCount > 0) {
1889
- console.log(` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
1890
- `);
1891
- } else {
1892
- console.log(` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
1893
- `);
1894
- }
1895
- });
1896
- 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) => {
1897
- const opts = buildOpts(cmd);
1898
- const context = getProjectContext(opts);
1899
- if (cmd.json) {
1900
- console.log(JSON.stringify(context, null, 2));
1901
- return;
1902
- }
1903
- console.log(`
1904
- ${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
1905
- console.log(` Project: ${c.cyan(context.projectPath)}`);
1906
- if (context.environment) {
1907
- console.log(` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`);
1908
- }
1909
- console.log(`
1910
- ${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`);
1911
- for (const s of context.secrets) {
1912
- const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
1913
- const flags = [];
1914
- if (s.requiresApproval) flags.push(c.yellow("locked"));
1915
- if (s.jitProvider) flags.push(c.magenta("jit"));
1916
- if (s.hasStates) flags.push(c.blue("superposition"));
1917
- if (s.isExpired) flags.push(c.red("expired"));
1918
- else if (s.isStale) flags.push(c.yellow("stale"));
1919
- const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
1920
- console.log(` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`);
1921
- }
1922
- if (context.manifest) {
1923
- console.log(`
1924
- ${c.bold(" Manifest:")} ${context.manifest.declared} declared`);
1925
- if (context.manifest.missing.length > 0) {
1926
- console.log(` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`);
1927
- } else {
1928
- console.log(` ${c.green(SYMBOLS.check)} All manifest secrets present`);
1929
- }
1930
- }
1931
- console.log(`
1932
- ${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`);
1933
- console.log(`${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`);
1934
- console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
1935
- if (context.recentActions.length > 0) {
1936
- console.log(`
1937
- ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`);
1938
- for (const a of context.recentActions.slice(0, 8)) {
1939
- const ts = new Date(a.timestamp).toLocaleTimeString();
1940
- console.log(` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`);
1941
- }
1942
- }
1943
- console.log();
1944
- });
1945
- program2.command("remember <key> <value>").description("Store a key-value pair in encrypted agent memory (persists across sessions)").action((key, value) => {
1946
- remember(key, value);
1947
- console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
1948
- });
1949
- program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
1950
- if (!key) {
1951
- const entries = listMemory();
1952
- if (entries.length === 0) {
1953
- console.log(c.dim("Agent memory is empty."));
1954
- return;
1955
- }
1956
- console.log(`
1957
- ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
1958
- `);
1959
- for (const e of entries) {
1960
- console.log(` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`);
1961
- }
1962
- console.log();
1963
- return;
1964
- }
1965
- const value = recall(key);
1966
- if (value === null) {
1967
- console.log(c.dim(`No memory found for "${key}"`));
1968
- } else {
1969
- console.log(safeStr(value));
1970
- }
1971
- });
1972
- program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
1973
- if (cmd.all) {
1974
- clearMemory();
1975
- console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
1976
- return;
1977
- }
1978
- const removed = forget(key);
1979
- if (removed) {
1980
- console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
1981
- } else {
1982
- console.log(c.dim(`No memory found for "${key}"`));
1983
- }
1984
- });
1985
- 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) => {
1986
- const opts = buildOpts(cmd);
1987
- const scope = opts.scope ?? "global";
1988
- if (cmd.list) {
1989
- const approvals = listApprovals();
1990
- if (approvals.length === 0) {
1991
- console.log(c.dim(" No active approvals"));
1992
- return;
1993
- }
1994
- for (const a of approvals) {
1995
- const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
1996
- const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
1997
- console.log(` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`);
1998
- }
1999
- return;
2000
- }
2001
- if (cmd.revoke) {
2002
- const revoked = revokeApproval(key, scope);
2003
- if (revoked) {
2004
- console.log(`${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`);
2005
- } else {
2006
- console.log(c.dim(` No active approval found for ${key}`));
1122
+ if (warnings.length > 0 && process.stderr.isTTY) {
1123
+ console.error();
1124
+ for (const w of warnings) {
1125
+ console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
2007
1126
  }
2008
- return;
2009
- }
2010
- const entry = grantApproval(key, scope, cmd.for, {
2011
- reason: cmd.reason ?? "manual approval"
2012
- });
2013
- console.log(`${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`);
2014
- console.log(c.dim(` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`));
2015
- });
2016
- program2.command("approvals").description("List all approval tokens with verification status").action(() => {
2017
- const approvals = listApprovals();
2018
- if (approvals.length === 0) {
2019
- console.log(c.dim(" No approvals found"));
2020
- return;
2021
- }
2022
- console.log(c.bold("\n\u{1F510} Approval Tokens\n"));
2023
- for (const a of approvals) {
2024
- const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
2025
- const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
2026
- console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
2027
- console.log(c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`));
2028
- if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
2029
- }
2030
- console.log();
2031
- });
2032
- program2.command("hook:install").description("Install a git pre-commit hook that scans for hardcoded secrets").option("--project-path <path>", "Repository path").action((cmd) => {
2033
- const result = installPreCommitHook(cmd.projectPath);
2034
- if (result.installed) {
2035
- console.log(`${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`);
2036
- } else {
2037
- console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
2038
- }
2039
- });
2040
- program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
2041
- const removed = uninstallPreCommitHook(cmd.projectPath);
2042
- if (removed) {
2043
- console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
2044
- } else {
2045
- console.log(c.dim("No q-ring pre-commit hook found"));
1127
+ console.error();
2046
1128
  }
2047
1129
  });
2048
- program2.command("hook:run").description("Run the pre-commit secret scan (called by the git hook)").action(() => {
2049
- const code = runPreCommitScan();
2050
- process.exit(code);
2051
- });
2052
- 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) => {
1130
+ program2.command("wizard <name>").description(
1131
+ "Set up a new service integration with secrets, manifest, and hooks"
1132
+ ).option(
1133
+ "--keys <keys>",
1134
+ "Comma-separated secret key names to create (e.g. API_KEY,API_SECRET)"
1135
+ ).option(
1136
+ "--provider <provider>",
1137
+ "Validation provider (e.g. openai, stripe, github)"
1138
+ ).option("--tags <tags>", "Comma-separated tags for all secrets").option(
1139
+ "--hook-exec <cmd>",
1140
+ "Shell command to run when any of these secrets change"
1141
+ ).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").action(async (name, cmd) => {
2053
1142
  const opts = buildOpts(cmd);
2054
1143
  const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2055
- const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [name.toLowerCase()];
1144
+ const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [
1145
+ name.toLowerCase()
1146
+ ];
2056
1147
  const provider = cmd.provider;
2057
1148
  let keyNames;
2058
1149
  if (cmd.keys) {
@@ -2064,7 +1155,10 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2064
1155
  ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
2065
1156
  `);
2066
1157
  for (const key of keyNames) {
2067
- const value = generateSecret({ format: "api-key", prefix: `${prefix.toLowerCase()}_` });
1158
+ const value = generateSecret({
1159
+ format: "api-key",
1160
+ prefix: `${prefix.toLowerCase()}_`
1161
+ });
2068
1162
  setSecret(key, value, {
2069
1163
  ...opts,
2070
1164
  tags,
@@ -2077,8 +1171,8 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
2077
1171
  const manifestPath = `${projectPath}/.q-ring.json`;
2078
1172
  let config = {};
2079
1173
  try {
2080
- if (existsSync4(manifestPath)) {
2081
- config = JSON.parse(readFileSync6(manifestPath, "utf8"));
1174
+ if (existsSync(manifestPath)) {
1175
+ config = JSON.parse(readFileSync2(manifestPath, "utf8"));
2082
1176
  }
2083
1177
  } catch {
2084
1178
  }
@@ -2090,119 +1184,162 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
2090
1184
  ...provider ? { provider } : {}
2091
1185
  };
2092
1186
  }
2093
- writeFileSync4(manifestPath, JSON.stringify(config, null, 2) + "\n", "utf8");
2094
- console.log(` ${c.green(SYMBOLS.check)} Updated ${c.dim(".q-ring.json")} manifest`);
1187
+ writeFileSync(manifestPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1188
+ console.log(
1189
+ ` ${c.green(SYMBOLS.check)} Updated ${c.dim(".q-ring.json")} manifest`
1190
+ );
2095
1191
  if (cmd.hookExec) {
2096
1192
  for (const key of keyNames) {
2097
1193
  registerHook({
2098
1194
  type: "shell",
2099
- match: { key, action: ["write", "delete"] },
1195
+ match: {
1196
+ key,
1197
+ action: ["write", "delete"]
1198
+ },
2100
1199
  command: cmd.hookExec,
2101
1200
  description: `${name} wizard hook`,
2102
1201
  enabled: true
2103
1202
  });
2104
1203
  }
2105
- console.log(` ${c.green(SYMBOLS.check)} Registered hook: ${c.dim(cmd.hookExec)}`);
1204
+ console.log(
1205
+ ` ${c.green(SYMBOLS.check)} Registered hook: ${c.dim(cmd.hookExec)}`
1206
+ );
2106
1207
  }
2107
- console.log(`
1208
+ console.log(
1209
+ `
2108
1210
  ${c.green("Done!")} Service "${name}" is ready with ${keyNames.length} secrets.
2109
- `);
1211
+ `
1212
+ );
2110
1213
  });
2111
- 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) => {
2112
- const opts = buildOpts(cmd);
2113
- const entries = listSecrets({ ...opts, silent: true });
2114
- const audit = queryAudit({ limit: 1e3 });
2115
- console.log(`
2116
- ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2117
- `);
2118
- const accessMap = /* @__PURE__ */ new Map();
2119
- for (const e of audit) {
2120
- if (e.action === "read" && e.key) {
2121
- accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
2122
- }
2123
- }
2124
- const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
2125
- if (sorted.length > 0) {
2126
- console.log(` ${c.bold("Most accessed:")}`);
2127
- for (const [key, count] of sorted.slice(0, 5)) {
2128
- console.log(` ${c.bold(key)} \u2014 ${c.cyan(count.toString())} reads`);
2129
- }
2130
- console.log();
2131
- }
2132
- const neverAccessed = entries.filter((e) => {
2133
- const count = e.envelope?.meta.accessCount ?? 0;
2134
- return count === 0;
2135
- });
2136
- if (neverAccessed.length > 0) {
2137
- console.log(` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`);
2138
- for (const e of neverAccessed.slice(0, 8)) {
2139
- const age = e.envelope?.meta.createdAt ? c.dim(`(created ${new Date(e.envelope.meta.createdAt).toLocaleDateString()})`) : "";
2140
- console.log(` ${c.dim(SYMBOLS.cross)} ${e.key} ${age}`);
2141
- }
2142
- console.log();
2143
- }
2144
- const expired = entries.filter((e) => e.decay?.isExpired);
2145
- const stale = entries.filter((e) => e.decay?.isStale && !e.decay?.isExpired);
2146
- if (expired.length > 0) {
2147
- console.log(` ${c.red("Expired:")} ${expired.length} secrets need rotation or cleanup`);
2148
- for (const e of expired.slice(0, 5)) {
2149
- console.log(` ${c.red(SYMBOLS.cross)} ${e.key}`);
2150
- }
2151
- console.log();
2152
- }
2153
- if (stale.length > 0) {
2154
- console.log(` ${c.yellow("Stale (>75% lifetime):")} ${stale.length} secrets approaching expiry`);
2155
- for (const e of stale.slice(0, 5)) {
2156
- console.log(` ${c.yellow(SYMBOLS.warning)} ${e.key} ${c.dim(`(${e.decay?.timeRemaining} remaining)`)}`);
2157
- }
2158
- console.log();
2159
- }
2160
- const globalOnly = entries.filter((e) => e.scope === "global");
2161
- const withProjectTags = globalOnly.filter(
2162
- (e) => e.envelope?.meta.tags?.some((t) => ["backend", "frontend", "db", "api"].includes(t))
1214
+ }
1215
+
1216
+ // src/core/teleport.ts
1217
+ import {
1218
+ randomBytes as randomBytes2,
1219
+ createCipheriv,
1220
+ createDecipheriv,
1221
+ pbkdf2Sync
1222
+ } from "crypto";
1223
+ import { z } from "zod";
1224
+ var ALGORITHM = "aes-256-gcm";
1225
+ var KEY_LENGTH = 32;
1226
+ var IV_LENGTH = 12;
1227
+ var SALT_LENGTH = 32;
1228
+ var PBKDF2_ITERATIONS = 1e5;
1229
+ var TeleportBundleSchema = z.object({
1230
+ v: z.literal(1),
1231
+ data: z.string(),
1232
+ salt: z.string(),
1233
+ iv: z.string(),
1234
+ tag: z.string(),
1235
+ createdAt: z.string(),
1236
+ count: z.number()
1237
+ });
1238
+ var TeleportPayloadSchema = z.object({
1239
+ secrets: z.array(
1240
+ z.object({
1241
+ key: z.string(),
1242
+ value: z.string(),
1243
+ scope: z.string().optional()
1244
+ })
1245
+ ),
1246
+ exportedAt: z.string(),
1247
+ exportedBy: z.string().optional()
1248
+ });
1249
+ function deriveKey(passphrase, salt) {
1250
+ return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
1251
+ }
1252
+ function teleportPack(secrets, passphrase) {
1253
+ const payload = {
1254
+ secrets,
1255
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
1256
+ };
1257
+ const plaintext = JSON.stringify(payload);
1258
+ const salt = randomBytes2(SALT_LENGTH);
1259
+ const iv = randomBytes2(IV_LENGTH);
1260
+ const key = deriveKey(passphrase, salt);
1261
+ const cipher = createCipheriv(ALGORITHM, key, iv);
1262
+ const encrypted = Buffer.concat([
1263
+ cipher.update(plaintext, "utf8"),
1264
+ cipher.final()
1265
+ ]);
1266
+ const tag = cipher.getAuthTag();
1267
+ const bundle = {
1268
+ v: 1,
1269
+ data: encrypted.toString("base64"),
1270
+ salt: salt.toString("base64"),
1271
+ iv: iv.toString("base64"),
1272
+ tag: tag.toString("base64"),
1273
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1274
+ count: secrets.length
1275
+ };
1276
+ return Buffer.from(JSON.stringify(bundle)).toString("base64");
1277
+ }
1278
+ function teleportUnpack(encoded, passphrase) {
1279
+ let bundleJson;
1280
+ try {
1281
+ bundleJson = Buffer.from(encoded, "base64").toString("utf8");
1282
+ } catch {
1283
+ throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
1284
+ }
1285
+ let rawBundle;
1286
+ try {
1287
+ rawBundle = JSON.parse(bundleJson);
1288
+ } catch {
1289
+ throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
1290
+ }
1291
+ const parsedBundle = TeleportBundleSchema.safeParse(rawBundle);
1292
+ if (!parsedBundle.success) {
1293
+ throw new Error(
1294
+ `ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
2163
1295
  );
2164
- if (withProjectTags.length > 0) {
2165
- console.log(` ${c.bold("Scope suggestions:")}`);
2166
- console.log(` ${withProjectTags.length} global secret(s) have project-specific tags \u2014 consider moving to project scope`);
2167
- console.log();
2168
- }
2169
- const noRotation = entries.filter(
2170
- (e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
1296
+ }
1297
+ const bundle = parsedBundle.data;
1298
+ const salt = Buffer.from(bundle.salt, "base64");
1299
+ const iv = Buffer.from(bundle.iv, "base64");
1300
+ const tag = Buffer.from(bundle.tag, "base64");
1301
+ const encrypted = Buffer.from(bundle.data, "base64");
1302
+ const key = deriveKey(passphrase, salt);
1303
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
1304
+ decipher.setAuthTag(tag);
1305
+ let decrypted;
1306
+ try {
1307
+ decrypted = Buffer.concat([
1308
+ decipher.update(encrypted),
1309
+ decipher.final()
1310
+ ]);
1311
+ } catch {
1312
+ throw new Error("ERR_TELEPORT_BAD_PASSPHRASE: decryption failed (wrong passphrase or corrupt data)");
1313
+ }
1314
+ let rawPayload;
1315
+ try {
1316
+ rawPayload = JSON.parse(decrypted.toString("utf8"));
1317
+ } catch {
1318
+ throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
1319
+ }
1320
+ const payload = TeleportPayloadSchema.safeParse(rawPayload);
1321
+ if (!payload.success) {
1322
+ throw new Error(
1323
+ `ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
2171
1324
  );
2172
- if (noRotation.length > 0) {
2173
- console.log(` ${c.bold("Rotation suggestions:")}`);
2174
- console.log(` ${noRotation.length} secret(s) have no rotation format set`);
2175
- console.log(` Use ${c.bold("qring set <key> <value> --rotation-format api-key")} to enable auto-rotation`);
2176
- console.log();
2177
- }
2178
- console.log(` ${c.bold("Summary:")}`);
2179
- console.log(` Total secrets: ${entries.length}`);
2180
- console.log(` Active: ${entries.length - expired.length}`);
2181
- console.log(` Expired: ${expired.length}`);
2182
- console.log(` Stale: ${stale.length}`);
2183
- console.log(` Never accessed: ${neverAccessed.length}`);
2184
- console.log(` With rotation config: ${entries.length - noRotation.length}`);
2185
- console.log();
2186
- });
2187
- program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
2188
- const result = collapseEnvironment({
2189
- projectPath: cmd.projectPath ?? process.cwd()
2190
- });
2191
- if (result) {
2192
- console.log(
2193
- `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
2194
- );
2195
- } else {
2196
- console.log(
2197
- c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
2198
- );
2199
- }
2200
- });
1325
+ }
1326
+ return payload.data;
1327
+ }
1328
+
1329
+ // src/cli/commands/quantum.ts
1330
+ function registerQuantumCommands(program2) {
2201
1331
  program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
2202
1332
  "-f, --format <format>",
2203
1333
  "Format: hex, base64, alphanumeric, uuid, api-key, token, password",
2204
1334
  "api-key"
2205
- ).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) => {
1335
+ ).option(
1336
+ "-l, --length <n>",
1337
+ "Length (bytes or chars depending on format)",
1338
+ parseInt
1339
+ ).option("--prefix <prefix>", "Prefix for api-key/token format").option(
1340
+ "-s, --save <key>",
1341
+ "Save the generated secret to keyring with this key"
1342
+ ).option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
2206
1343
  const secret = generateSecret({
2207
1344
  format: cmd.format,
2208
1345
  length: cmd.length,
@@ -2225,42 +1362,38 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
2225
1362
  }
2226
1363
  }
2227
1364
  });
2228
- 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(
2229
- (sourceKey, targetKey, cmd) => {
2230
- const sourceOpts = {
2231
- scope: cmd.sourceProject ? "project" : "global",
2232
- projectPath: cmd.sourceProject ?? process.cwd(),
2233
- source: "cli"
2234
- };
2235
- const targetOpts = {
2236
- scope: cmd.targetProject ? "project" : "global",
2237
- projectPath: cmd.targetProject ?? process.cwd(),
2238
- source: "cli"
2239
- };
2240
- entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
2241
- console.log(
2242
- `${SYMBOLS.link} ${c.cyan("entangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
2243
- );
2244
- }
2245
- );
2246
- 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(
2247
- (sourceKey, targetKey, cmd) => {
2248
- const sourceOpts = {
2249
- scope: cmd.sourceProject ? "project" : "global",
2250
- projectPath: cmd.sourceProject ?? process.cwd(),
2251
- source: "cli"
2252
- };
2253
- const targetOpts = {
2254
- scope: cmd.targetProject ? "project" : "global",
2255
- projectPath: cmd.targetProject ?? process.cwd(),
2256
- source: "cli"
2257
- };
2258
- disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
2259
- console.log(
2260
- `${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
2261
- );
2262
- }
2263
- );
1365
+ 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((sourceKey, targetKey, cmd) => {
1366
+ const sourceOpts = {
1367
+ scope: cmd.sourceProject ? "project" : "global",
1368
+ projectPath: cmd.sourceProject ?? process.cwd(),
1369
+ source: "cli"
1370
+ };
1371
+ const targetOpts = {
1372
+ scope: cmd.targetProject ? "project" : "global",
1373
+ projectPath: cmd.targetProject ?? process.cwd(),
1374
+ source: "cli"
1375
+ };
1376
+ entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
1377
+ console.log(
1378
+ `${SYMBOLS.link} ${c.cyan("entangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
1379
+ );
1380
+ });
1381
+ 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((sourceKey, targetKey, cmd) => {
1382
+ const sourceOpts = {
1383
+ scope: cmd.sourceProject ? "project" : "global",
1384
+ projectPath: cmd.sourceProject ?? process.cwd(),
1385
+ source: "cli"
1386
+ };
1387
+ const targetOpts = {
1388
+ scope: cmd.targetProject ? "project" : "global",
1389
+ projectPath: cmd.targetProject ?? process.cwd(),
1390
+ source: "cli"
1391
+ };
1392
+ disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
1393
+ console.log(
1394
+ `${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
1395
+ );
1396
+ });
2264
1397
  const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
2265
1398
  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) => {
2266
1399
  const id = tunnelCreate(value, {
@@ -2277,118 +1410,881 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
2277
1410
  const value = tunnelRead(id);
2278
1411
  if (value === null) {
2279
1412
  console.error(
2280
- c.red(`${SYMBOLS.cross} Tunnel "${id}" not found or expired`)
1413
+ c.red(`${SYMBOLS.cross} Tunnel "${id}" not found or expired`)
1414
+ );
1415
+ process.exit(1);
1416
+ }
1417
+ process.stdout.write(value);
1418
+ });
1419
+ tunnel.command("destroy <id>").description("Destroy a tunneled secret immediately").action((id) => {
1420
+ const destroyed = tunnelDestroy(id);
1421
+ if (destroyed) {
1422
+ console.log(`${SYMBOLS.check} ${c.green("destroyed")} ${id}`);
1423
+ } else {
1424
+ console.error(c.red(`${SYMBOLS.cross} Tunnel "${id}" not found`));
1425
+ process.exit(1);
1426
+ }
1427
+ });
1428
+ tunnel.command("list").alias("ls").description("List active tunnels").action(() => {
1429
+ const tunnels = tunnelList();
1430
+ if (tunnels.length === 0) {
1431
+ console.log(c.dim("No active tunnels"));
1432
+ return;
1433
+ }
1434
+ console.log(
1435
+ c.bold(`
1436
+ ${SYMBOLS.ghost} Active tunnels (${tunnels.length})
1437
+ `)
1438
+ );
1439
+ for (const t of tunnels) {
1440
+ const parts = [c.bold(t.id)];
1441
+ parts.push(c.dim(`reads: ${t.accessCount}`));
1442
+ if (t.maxReads) parts.push(c.dim(`max: ${t.maxReads}`));
1443
+ if (t.expiresAt) {
1444
+ const remaining = Math.max(
1445
+ 0,
1446
+ Math.floor((t.expiresAt - Date.now()) / 1e3)
1447
+ );
1448
+ parts.push(c.dim(`expires: ${remaining}s`));
1449
+ }
1450
+ console.log(` ${SYMBOLS.ghost} ${parts.join(" ")}`);
1451
+ }
1452
+ console.log();
1453
+ });
1454
+ const tp = program2.command("teleport").alias("tp").description("Encrypted secret sharing (quantum teleportation)");
1455
+ tp.command("pack").description("Pack secrets into an encrypted bundle").option("-k, --keys <keys>", "Comma-separated key names to pack").option("-g, --global", "Pack global scope").option("-p, --project", "Pack project scope").option("--project-path <path>", "Explicit project path").action(async (cmd) => {
1456
+ const opts = buildOpts(cmd);
1457
+ const passphrase = await promptSecret(
1458
+ `${SYMBOLS.lock} Enter passphrase for encryption: `
1459
+ );
1460
+ if (!passphrase) {
1461
+ console.error(c.red("Passphrase required"));
1462
+ process.exit(1);
1463
+ }
1464
+ const entries = listSecrets(opts);
1465
+ let keys;
1466
+ if (cmd.keys) {
1467
+ keys = cmd.keys.split(",").map((k) => k.trim());
1468
+ }
1469
+ const secrets = [];
1470
+ for (const entry of entries) {
1471
+ if (keys && !keys.includes(entry.key)) continue;
1472
+ const value = getSecret(entry.key, { ...opts, scope: entry.scope });
1473
+ if (value !== null) {
1474
+ secrets.push({ key: entry.key, value, scope: entry.scope });
1475
+ }
1476
+ }
1477
+ if (secrets.length === 0) {
1478
+ console.error(c.red("No secrets to pack"));
1479
+ process.exit(1);
1480
+ }
1481
+ const bundle = teleportPack(secrets, passphrase);
1482
+ process.stdout.write(bundle);
1483
+ if (process.stdout.isTTY) {
1484
+ console.log(
1485
+ `
1486
+ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
1487
+ );
1488
+ }
1489
+ });
1490
+ tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").action(async (bundle, cmd) => {
1491
+ if (!bundle) {
1492
+ const chunks = [];
1493
+ for await (const chunk of process.stdin) {
1494
+ chunks.push(chunk);
1495
+ }
1496
+ bundle = Buffer.concat(chunks).toString("utf8").trim();
1497
+ }
1498
+ const passphrase = await promptSecret(
1499
+ `${SYMBOLS.lock} Enter passphrase for decryption: `
1500
+ );
1501
+ try {
1502
+ const payload = teleportUnpack(bundle, passphrase);
1503
+ if (cmd.dryRun) {
1504
+ console.log(
1505
+ `
1506
+ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
1507
+ `
1508
+ );
1509
+ for (const s of payload.secrets) {
1510
+ console.log(
1511
+ ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`
1512
+ );
1513
+ }
1514
+ return;
1515
+ }
1516
+ const opts = buildOpts(cmd);
1517
+ for (const s of payload.secrets) {
1518
+ setSecret(s.key, s.value, opts);
1519
+ }
1520
+ console.log(
1521
+ `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
1522
+ );
1523
+ } catch {
1524
+ console.error(
1525
+ c.red(
1526
+ `${SYMBOLS.cross} Failed to unpack: wrong passphrase or corrupted bundle`
1527
+ )
1528
+ );
1529
+ process.exit(1);
1530
+ }
1531
+ });
1532
+ }
1533
+
1534
+ // src/cli/commands/validation.ts
1535
+ function registerValidationCommands(program2) {
1536
+ 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(
1537
+ "--provider <name>",
1538
+ "Force a specific provider (openai, stripe, github, aws, http)"
1539
+ ).option(
1540
+ "--all",
1541
+ "Validate all secrets that have a detectable provider"
1542
+ ).option(
1543
+ "--manifest",
1544
+ "Only validate manifest-declared secrets (with --all)"
1545
+ ).option("--list-providers", "List all available providers").action(async (key, cmd) => {
1546
+ if (cmd.listProviders) {
1547
+ console.log(
1548
+ c.bold(`
1549
+ ${SYMBOLS.shield} Available validation providers
1550
+ `)
1551
+ );
1552
+ for (const p of registry2.listProviders()) {
1553
+ const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
1554
+ console.log(
1555
+ ` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`
1556
+ );
1557
+ }
1558
+ console.log();
1559
+ return;
1560
+ }
1561
+ if (!key && !cmd.all) {
1562
+ console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
1563
+ process.exit(1);
1564
+ }
1565
+ const opts = buildOpts(cmd);
1566
+ if (cmd.all) {
1567
+ let entries = listSecrets(opts);
1568
+ const projectPath = cmd.projectPath ?? process.cwd();
1569
+ if (cmd.manifest) {
1570
+ const config = readProjectConfig(projectPath);
1571
+ if (config?.secrets) {
1572
+ const manifestKeys = new Set(Object.keys(config.secrets));
1573
+ entries = entries.filter((e) => manifestKeys.has(e.key));
1574
+ }
1575
+ }
1576
+ console.log(c.bold(`
1577
+ ${SYMBOLS.shield} Validating secrets
1578
+ `));
1579
+ let validated = 0;
1580
+ let skipped = 0;
1581
+ for (const entry of entries) {
1582
+ const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
1583
+ if (!value2) {
1584
+ skipped++;
1585
+ continue;
1586
+ }
1587
+ const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
1588
+ const result2 = await validateSecret(value2, { provider: provHint2 });
1589
+ if (result2.status === "unknown") {
1590
+ skipped++;
1591
+ continue;
1592
+ }
1593
+ validated++;
1594
+ const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
1595
+ const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
1596
+ console.log(
1597
+ ` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
1598
+ );
1599
+ }
1600
+ console.log(
1601
+ `
1602
+ ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
1603
+ `
1604
+ );
1605
+ return;
1606
+ }
1607
+ const value = getSecret(key, opts);
1608
+ if (!value) {
1609
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1610
+ process.exit(1);
1611
+ }
1612
+ const envelope = getEnvelope(key, opts);
1613
+ const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
1614
+ const result = await validateSecret(value, { provider: provHint });
1615
+ 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");
1616
+ console.log(
1617
+ `
1618
+ ${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`
1619
+ );
1620
+ if (result.message && result.status !== "valid") {
1621
+ console.log(` ${c.dim(result.message)}`);
1622
+ }
1623
+ console.log();
1624
+ });
1625
+ program2.command("rotate <key>").description(
1626
+ "Attempt issuer-native rotation of a secret via its provider"
1627
+ ).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) => {
1628
+ const opts = buildOpts(cmd);
1629
+ const value = getSecret(key, opts);
1630
+ if (!value) {
1631
+ console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
1632
+ process.exitCode = 1;
1633
+ return;
1634
+ }
1635
+ const result = await rotateWithProvider(value, cmd.provider);
1636
+ if (result.rotated && result.newValue) {
1637
+ setSecret(key, result.newValue, {
1638
+ ...opts,
1639
+ scope: opts.scope ?? "global"
1640
+ });
1641
+ console.log(
1642
+ `${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`
1643
+ );
1644
+ console.log(c.dim(` ${result.message}`));
1645
+ } else {
1646
+ console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
1647
+ }
1648
+ });
1649
+ program2.command("ci:validate").description(
1650
+ "CI-oriented batch validation of all secrets (exit code 1 on failure)"
1651
+ ).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) => {
1652
+ const opts = buildOpts(cmd);
1653
+ const entries = listSecrets(opts);
1654
+ const secrets = entries.map((e) => {
1655
+ const val = getSecret(e.key, {
1656
+ ...opts,
1657
+ scope: e.scope,
1658
+ silent: true
1659
+ });
1660
+ if (!val) return null;
1661
+ const provider = e.envelope?.meta.provider;
1662
+ const validationUrl = e.envelope?.meta.validationUrl;
1663
+ return { key: e.key, value: val, provider, validationUrl };
1664
+ }).filter((s) => s !== null);
1665
+ if (secrets.length === 0) {
1666
+ console.log(c.dim("No secrets to validate"));
1667
+ return;
1668
+ }
1669
+ const report = await ciValidateBatch(secrets);
1670
+ if (wantsJsonOutput(program2, cmd)) {
1671
+ console.log(JSON.stringify(report, null, 2));
1672
+ } else {
1673
+ console.log(
1674
+ c.bold(`
1675
+ CI Secret Validation: ${report.results.length} secrets
1676
+ `)
1677
+ );
1678
+ for (const r of report.results) {
1679
+ const icon = r.validation.valid ? SYMBOLS.check : SYMBOLS.cross;
1680
+ const color = r.validation.valid ? c.green : c.red;
1681
+ console.log(
1682
+ ` ${icon} ${color(r.key)} [${r.validation.provider}] ${r.validation.message}`
1683
+ );
1684
+ if (r.requiresRotation) console.log(c.dim(` \u2192 rotation required`));
1685
+ }
1686
+ console.log();
1687
+ if (!report.allValid) {
1688
+ console.log(c.red(` ${report.failCount} secret(s) failed validation`));
1689
+ process.exitCode = 1;
1690
+ } else {
1691
+ console.log(c.green(` All secrets valid`));
1692
+ }
1693
+ }
1694
+ });
1695
+ }
1696
+
1697
+ // src/core/exec.ts
1698
+ import { spawn } from "child_process";
1699
+ import { Transform } from "stream";
1700
+ var BUILTIN_PROFILES = {
1701
+ unrestricted: { name: "unrestricted" },
1702
+ restricted: {
1703
+ name: "restricted",
1704
+ denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
1705
+ maxRuntimeSeconds: 30,
1706
+ allowNetwork: false,
1707
+ stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
1708
+ },
1709
+ ci: {
1710
+ name: "ci",
1711
+ maxRuntimeSeconds: 300,
1712
+ allowNetwork: true,
1713
+ denyCommands: ["rm -rf /", "mkfs", "dd if="]
1714
+ }
1715
+ };
1716
+ function getProfile(name) {
1717
+ if (!name) return BUILTIN_PROFILES.unrestricted;
1718
+ return BUILTIN_PROFILES[name] ?? { name };
1719
+ }
1720
+ var RedactionTransform = class extends Transform {
1721
+ patterns = [];
1722
+ tail = "";
1723
+ maxLen = 0;
1724
+ constructor(secretsToRedact) {
1725
+ super();
1726
+ const validSecrets = secretsToRedact.filter((s) => s.length > 5);
1727
+ validSecrets.sort((a, b) => b.length - a.length);
1728
+ this.patterns = validSecrets.map((s) => ({
1729
+ value: s,
1730
+ replacement: "[QRING:REDACTED]"
1731
+ }));
1732
+ if (validSecrets.length > 0) {
1733
+ this.maxLen = validSecrets[0].length;
1734
+ }
1735
+ }
1736
+ _transform(chunk, _encoding, callback) {
1737
+ if (this.patterns.length === 0) {
1738
+ this.push(chunk);
1739
+ return callback();
1740
+ }
1741
+ const text = this.tail + chunk.toString();
1742
+ let redacted = text;
1743
+ for (const { value, replacement } of this.patterns) {
1744
+ redacted = redacted.split(value).join(replacement);
1745
+ }
1746
+ if (redacted.length < this.maxLen) {
1747
+ this.tail = redacted;
1748
+ return callback();
1749
+ }
1750
+ const outputLen = redacted.length - this.maxLen + 1;
1751
+ const output = redacted.slice(0, outputLen);
1752
+ this.tail = redacted.slice(outputLen);
1753
+ this.push(output);
1754
+ callback();
1755
+ }
1756
+ _flush(callback) {
1757
+ if (this.tail) {
1758
+ let final = this.tail;
1759
+ for (const { value, replacement } of this.patterns) {
1760
+ final = final.split(value).join(replacement);
1761
+ }
1762
+ this.push(final);
1763
+ }
1764
+ callback();
1765
+ }
1766
+ };
1767
+ async function execCommand(opts) {
1768
+ const profile = getProfile(opts.profile);
1769
+ const fullCommand = [opts.command, ...opts.args].join(" ");
1770
+ const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
1771
+ if (!policyDecision.allowed) {
1772
+ throw new Error(`Policy Denied: ${policyDecision.reason}`);
1773
+ }
1774
+ if (profile.denyCommands) {
1775
+ const denied = profile.denyCommands.find((d) => {
1776
+ const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
1777
+ return pattern.test(fullCommand);
1778
+ });
1779
+ if (denied) {
1780
+ throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
1781
+ }
1782
+ }
1783
+ if (profile.allowCommands) {
1784
+ const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
1785
+ if (!allowed) {
1786
+ throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
1787
+ }
1788
+ }
1789
+ const envMap = {};
1790
+ for (const [k, v] of Object.entries(process.env)) {
1791
+ if (v !== void 0) envMap[k] = v;
1792
+ }
1793
+ if (profile.stripEnvVars) {
1794
+ for (const key of profile.stripEnvVars) {
1795
+ delete envMap[key];
1796
+ }
1797
+ }
1798
+ const secretsToRedact = /* @__PURE__ */ new Set();
1799
+ let entries = listSecrets({
1800
+ scope: opts.scope,
1801
+ projectPath: opts.projectPath,
1802
+ source: opts.source ?? "cli",
1803
+ silent: true
1804
+ // list silently
1805
+ });
1806
+ if (opts.keys?.length) {
1807
+ const keySet = new Set(opts.keys);
1808
+ entries = entries.filter((e) => keySet.has(e.key));
1809
+ }
1810
+ if (opts.tags?.length) {
1811
+ entries = entries.filter(
1812
+ (e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
1813
+ );
1814
+ }
1815
+ for (const entry of entries) {
1816
+ if (entry.envelope) {
1817
+ const decay = checkDecay(entry.envelope);
1818
+ if (decay.isExpired) continue;
1819
+ }
1820
+ const val = getSecret(entry.key, {
1821
+ scope: entry.scope,
1822
+ projectPath: opts.projectPath,
1823
+ env: opts.env,
1824
+ source: opts.source ?? "cli",
1825
+ silent: false
1826
+ // Log access for execution
1827
+ });
1828
+ if (val !== null) {
1829
+ envMap[entry.key] = val;
1830
+ if (val.length > 5) {
1831
+ secretsToRedact.add(val);
1832
+ }
1833
+ }
1834
+ }
1835
+ const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
1836
+ return new Promise((resolve, reject) => {
1837
+ const networkTools = /* @__PURE__ */ new Set([
1838
+ "curl",
1839
+ "wget",
1840
+ "ping",
1841
+ "nc",
1842
+ "netcat",
1843
+ "ssh",
1844
+ "telnet",
1845
+ "ftp",
1846
+ "dig",
1847
+ "nslookup"
1848
+ ]);
1849
+ if (profile.allowNetwork === false && networkTools.has(opts.command)) {
1850
+ const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
1851
+ if (opts.captureOutput) {
1852
+ return resolve({ code: 126, stdout: "", stderr: msg });
1853
+ }
1854
+ process.stderr.write(msg + "\n");
1855
+ return resolve({ code: 126, stdout: "", stderr: "" });
1856
+ }
1857
+ const child = spawn(opts.command, opts.args, {
1858
+ env: envMap,
1859
+ stdio: ["inherit", "pipe", "pipe"],
1860
+ shell: false
1861
+ });
1862
+ let timedOut = false;
1863
+ let timer;
1864
+ if (maxRuntime) {
1865
+ timer = setTimeout(() => {
1866
+ timedOut = true;
1867
+ child.kill("SIGKILL");
1868
+ }, maxRuntime * 1e3);
1869
+ }
1870
+ const stdoutRedact = new RedactionTransform([...secretsToRedact]);
1871
+ const stderrRedact = new RedactionTransform([...secretsToRedact]);
1872
+ if (child.stdout) child.stdout.pipe(stdoutRedact);
1873
+ if (child.stderr) child.stderr.pipe(stderrRedact);
1874
+ let stdoutStr = "";
1875
+ let stderrStr = "";
1876
+ if (opts.captureOutput) {
1877
+ stdoutRedact.on("data", (d) => stdoutStr += d.toString());
1878
+ stderrRedact.on("data", (d) => stderrStr += d.toString());
1879
+ } else {
1880
+ stdoutRedact.pipe(process.stdout);
1881
+ stderrRedact.pipe(process.stderr);
1882
+ }
1883
+ child.on("close", (code) => {
1884
+ if (timer) clearTimeout(timer);
1885
+ if (timedOut) {
1886
+ resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
1887
+ [QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
1888
+ } else {
1889
+ resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
1890
+ }
1891
+ });
1892
+ child.on("error", (err) => {
1893
+ if (timer) clearTimeout(timer);
1894
+ reject(err);
1895
+ });
1896
+ });
1897
+ }
1898
+
1899
+ // src/core/scan.ts
1900
+ import { readFileSync as readFileSync3, readdirSync, statSync } from "fs";
1901
+ import { join } from "path";
1902
+
1903
+ // src/core/secrets-detect.ts
1904
+ var SECRET_ASSIGNMENT_PATTERN = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
1905
+ function calculateEntropy(str) {
1906
+ if (!str) return 0;
1907
+ const len = str.length;
1908
+ const frequencies = /* @__PURE__ */ new Map();
1909
+ for (let i = 0; i < len; i++) {
1910
+ const char = str[i];
1911
+ frequencies.set(char, (frequencies.get(char) || 0) + 1);
1912
+ }
1913
+ let entropy = 0;
1914
+ for (const count of frequencies.values()) {
1915
+ const p = count / len;
1916
+ entropy -= p * Math.log2(p);
1917
+ }
1918
+ return entropy;
1919
+ }
1920
+ function isPlaceholderValue(value) {
1921
+ const lv = value.toLowerCase();
1922
+ return lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx");
1923
+ }
1924
+ function passesSecretHeuristic(value, entropy) {
1925
+ return entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_");
1926
+ }
1927
+ function findSecretsInLine(line) {
1928
+ if (line.length > 500) return [];
1929
+ const out = [];
1930
+ SECRET_ASSIGNMENT_PATTERN.lastIndex = 0;
1931
+ let match;
1932
+ while ((match = SECRET_ASSIGNMENT_PATTERN.exec(line)) !== null) {
1933
+ const varName = match[1];
1934
+ const quote = match[2];
1935
+ const value = match[3];
1936
+ if (value.length < 8) continue;
1937
+ if (isPlaceholderValue(value)) continue;
1938
+ const entropy = calculateEntropy(value);
1939
+ if (!passesSecretHeuristic(value, entropy)) continue;
1940
+ out.push({ varName, value, quote });
1941
+ }
1942
+ return out;
1943
+ }
1944
+
1945
+ // src/core/scan.ts
1946
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
1947
+ "node_modules",
1948
+ ".git",
1949
+ ".next",
1950
+ "dist",
1951
+ "build",
1952
+ "coverage",
1953
+ ".cursor",
1954
+ "venv",
1955
+ "__pycache__"
1956
+ ]);
1957
+ var IGNORE_EXTS = /* @__PURE__ */ new Set([
1958
+ ".png",
1959
+ ".jpg",
1960
+ ".jpeg",
1961
+ ".gif",
1962
+ ".ico",
1963
+ ".svg",
1964
+ ".webp",
1965
+ ".mp4",
1966
+ ".mp3",
1967
+ ".wav",
1968
+ ".ogg",
1969
+ ".pdf",
1970
+ ".zip",
1971
+ ".tar",
1972
+ ".gz",
1973
+ ".xz",
1974
+ ".ttf",
1975
+ ".woff",
1976
+ ".woff2",
1977
+ ".eot",
1978
+ ".exe",
1979
+ ".dll",
1980
+ ".so",
1981
+ ".dylib",
1982
+ ".lock"
1983
+ ]);
1984
+ function scanCodebase(dir) {
1985
+ const results = [];
1986
+ function walk(currentDir) {
1987
+ let entries;
1988
+ try {
1989
+ entries = readdirSync(currentDir);
1990
+ } catch {
1991
+ return;
1992
+ }
1993
+ for (const entry of entries) {
1994
+ if (IGNORE_DIRS.has(entry)) continue;
1995
+ const fullPath = join(currentDir, entry);
1996
+ let stat;
1997
+ try {
1998
+ stat = statSync(fullPath);
1999
+ } catch {
2000
+ continue;
2001
+ }
2002
+ if (stat.isDirectory()) {
2003
+ walk(fullPath);
2004
+ } else if (stat.isFile()) {
2005
+ const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
2006
+ if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
2007
+ let content;
2008
+ try {
2009
+ content = readFileSync3(fullPath, "utf8");
2010
+ } catch {
2011
+ continue;
2012
+ }
2013
+ if (content.includes("\0")) continue;
2014
+ const lines = content.split(/\r?\n/);
2015
+ for (let i = 0; i < lines.length; i++) {
2016
+ const line = lines[i];
2017
+ const matches = findSecretsInLine(line);
2018
+ for (const m of matches) {
2019
+ const entropy = calculateEntropy(m.value);
2020
+ const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
2021
+ results.push({
2022
+ file: relPath || fullPath,
2023
+ line: i + 1,
2024
+ keyName: m.varName,
2025
+ match: m.value,
2026
+ context: line.trim(),
2027
+ entropy: parseFloat(entropy.toFixed(2))
2028
+ });
2029
+ }
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ walk(dir);
2035
+ return results;
2036
+ }
2037
+
2038
+ // src/core/linter.ts
2039
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
2040
+ import { basename, extname } from "path";
2041
+ var ENV_REF_BY_EXT = {
2042
+ ".ts": (k) => `process.env.${k}`,
2043
+ ".tsx": (k) => `process.env.${k}`,
2044
+ ".js": (k) => `process.env.${k}`,
2045
+ ".jsx": (k) => `process.env.${k}`,
2046
+ ".mjs": (k) => `process.env.${k}`,
2047
+ ".cjs": (k) => `process.env.${k}`,
2048
+ ".py": (k) => `os.environ["${k}"]`,
2049
+ ".rb": (k) => `ENV["${k}"]`,
2050
+ ".go": (k) => `os.Getenv("${k}")`,
2051
+ ".rs": (k) => `std::env::var("${k}")`,
2052
+ ".java": (k) => `System.getenv("${k}")`,
2053
+ ".kt": (k) => `System.getenv("${k}")`,
2054
+ ".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
2055
+ ".php": (k) => `getenv('${k}')`,
2056
+ ".sh": (k) => `\${${k}}`,
2057
+ ".bash": (k) => `\${${k}}`
2058
+ };
2059
+ function getEnvRef(filePath, keyName) {
2060
+ const ext = extname(filePath).toLowerCase();
2061
+ const formatter = ENV_REF_BY_EXT[ext];
2062
+ return formatter ? formatter(keyName) : `process.env.${keyName}`;
2063
+ }
2064
+ function lintFiles(files, opts = {}) {
2065
+ const results = [];
2066
+ for (const file of files) {
2067
+ if (!existsSync2(file)) continue;
2068
+ let content;
2069
+ try {
2070
+ content = readFileSync4(file, "utf8");
2071
+ } catch {
2072
+ continue;
2073
+ }
2074
+ if (content.includes("\0")) continue;
2075
+ const lines = content.split(/\r?\n/);
2076
+ const fixes = [];
2077
+ for (let i = 0; i < lines.length; i++) {
2078
+ const line = lines[i];
2079
+ const matches = findSecretsInLine(line);
2080
+ for (const m of matches) {
2081
+ const varNameUpper = m.varName.toUpperCase();
2082
+ const entropy = calculateEntropy(m.value);
2083
+ const shouldFix = opts.fix === true;
2084
+ if (shouldFix) {
2085
+ const envRef = getEnvRef(file, varNameUpper);
2086
+ fixes.push({
2087
+ line: i,
2088
+ original: `${m.quote}${m.value}${m.quote}`,
2089
+ replacement: envRef,
2090
+ keyName: varNameUpper,
2091
+ value: m.value
2092
+ });
2093
+ }
2094
+ results.push({
2095
+ file,
2096
+ line: i + 1,
2097
+ keyName: varNameUpper,
2098
+ match: m.value,
2099
+ context: line.trim(),
2100
+ entropy: parseFloat(entropy.toFixed(2)),
2101
+ fixed: shouldFix
2102
+ });
2103
+ }
2104
+ }
2105
+ if (opts.fix && fixes.length > 0) {
2106
+ const fixLines = content.split(/\r?\n/);
2107
+ for (const fix of fixes.reverse()) {
2108
+ const lineIdx = fix.line;
2109
+ if (lineIdx >= 0 && lineIdx < fixLines.length) {
2110
+ fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
2111
+ }
2112
+ if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
2113
+ setSecret(fix.keyName, fix.value, {
2114
+ scope: opts.scope ?? "global",
2115
+ projectPath: opts.projectPath,
2116
+ source: "cli",
2117
+ description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
2118
+ });
2119
+ }
2120
+ }
2121
+ writeFileSync2(file, fixLines.join("\n"), "utf8");
2122
+ }
2123
+ }
2124
+ return results;
2125
+ }
2126
+
2127
+ // src/cli/commands/tooling.ts
2128
+ function registerToolingCommands(program2) {
2129
+ program2.command("exec <command...>").description(
2130
+ "Run a command with secrets injected into its environment (output auto-redacted)"
2131
+ ).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(
2132
+ "--profile <name>",
2133
+ "Exec profile: unrestricted, restricted, ci"
2134
+ ).action(async (commandArgs, cmd) => {
2135
+ const opts = buildOpts(cmd);
2136
+ const command = commandArgs[0];
2137
+ const args = commandArgs.slice(1);
2138
+ try {
2139
+ const { code } = await execCommand({
2140
+ ...opts,
2141
+ command,
2142
+ args,
2143
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
2144
+ tags: cmd.tags?.split(",").map((t) => t.trim()),
2145
+ profile: cmd.profile
2146
+ });
2147
+ process.exit(code);
2148
+ } catch (err) {
2149
+ console.error(
2150
+ c.red(
2151
+ `${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`
2152
+ )
2281
2153
  );
2282
2154
  process.exit(1);
2283
2155
  }
2284
- process.stdout.write(value);
2285
2156
  });
2286
- tunnel.command("destroy <id>").description("Destroy a tunneled secret immediately").action((id) => {
2287
- const destroyed = tunnelDestroy(id);
2288
- if (destroyed) {
2289
- console.log(`${SYMBOLS.check} ${c.green("destroyed")} ${id}`);
2290
- } else {
2291
- console.error(
2292
- c.red(`${SYMBOLS.cross} Tunnel "${id}" not found`)
2157
+ program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option(
2158
+ "--fix",
2159
+ "Auto-replace hardcoded secrets with process.env references and store in q-ring"
2160
+ ).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) => {
2161
+ const targetDir = dir ?? process.cwd();
2162
+ const fixMode = cmd.fix === true;
2163
+ console.log(
2164
+ `
2165
+ ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
2166
+ `
2167
+ );
2168
+ const results = scanCodebase(targetDir);
2169
+ if (results.length === 0) {
2170
+ console.log(
2171
+ ` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
2172
+ `
2293
2173
  );
2294
- process.exit(1);
2174
+ return;
2295
2175
  }
2296
- });
2297
- tunnel.command("list").alias("ls").description("List active tunnels").action(() => {
2298
- const tunnels = tunnelList();
2299
- if (tunnels.length === 0) {
2300
- console.log(c.dim("No active tunnels"));
2176
+ if (fixMode) {
2177
+ const fileSet = new Set(
2178
+ results.map(
2179
+ (r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`
2180
+ )
2181
+ );
2182
+ const opts = buildOpts(cmd);
2183
+ const lintResults = lintFiles([...fileSet], {
2184
+ fix: true,
2185
+ scope: opts.scope,
2186
+ projectPath: opts.projectPath
2187
+ });
2188
+ const fixedCount = lintResults.filter((r) => r.fixed).length;
2189
+ console.log(
2190
+ ` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
2191
+ `
2192
+ );
2301
2193
  return;
2302
2194
  }
2303
- console.log(c.bold(`
2304
- ${SYMBOLS.ghost} Active tunnels (${tunnels.length})
2305
- `));
2306
- for (const t of tunnels) {
2307
- const parts = [c.bold(t.id)];
2308
- parts.push(c.dim(`reads: ${t.accessCount}`));
2309
- if (t.maxReads) parts.push(c.dim(`max: ${t.maxReads}`));
2310
- if (t.expiresAt) {
2311
- const remaining = Math.max(0, Math.floor((t.expiresAt - Date.now()) / 1e3));
2312
- parts.push(c.dim(`expires: ${remaining}s`));
2313
- }
2314
- console.log(` ${SYMBOLS.ghost} ${parts.join(" ")}`);
2195
+ for (const res of results) {
2196
+ console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
2197
+ console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
2198
+ console.log(
2199
+ ` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`
2200
+ );
2201
+ console.log(` ${c.dim("Context:")} ${res.context}`);
2202
+ console.log();
2315
2203
  }
2316
- console.log();
2204
+ console.log(
2205
+ ` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
2206
+ `
2207
+ );
2317
2208
  });
2318
- const tp = program2.command("teleport").alias("tp").description("Encrypted secret sharing (quantum teleportation)");
2319
- tp.command("pack").description("Pack secrets into an encrypted bundle").option("-k, --keys <keys>", "Comma-separated key names to pack").option("-g, --global", "Pack global scope").option("-p, --project", "Pack project scope").option("--project-path <path>", "Explicit project path").action(async (cmd) => {
2209
+ program2.command("lint <files...>").description(
2210
+ "Lint specific files for hardcoded secrets (with optional auto-fix)"
2211
+ ).option(
2212
+ "--fix",
2213
+ "Replace hardcoded secrets with process.env references and store in q-ring"
2214
+ ).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) => {
2320
2215
  const opts = buildOpts(cmd);
2321
- const passphrase = await promptSecret(
2322
- `${SYMBOLS.lock} Enter passphrase for encryption: `
2323
- );
2324
- if (!passphrase) {
2325
- console.error(c.red("Passphrase required"));
2326
- process.exit(1);
2327
- }
2328
- const entries = listSecrets(opts);
2329
- let keys;
2330
- if (cmd.keys) {
2331
- keys = cmd.keys.split(",").map((k) => k.trim());
2332
- }
2333
- const secrets = [];
2334
- for (const entry of entries) {
2335
- if (keys && !keys.includes(entry.key)) continue;
2336
- const value = getSecret(entry.key, { ...opts, scope: entry.scope });
2337
- if (value !== null) {
2338
- secrets.push({ key: entry.key, value, scope: entry.scope });
2339
- }
2340
- }
2341
- if (secrets.length === 0) {
2342
- console.error(c.red("No secrets to pack"));
2343
- process.exit(1);
2344
- }
2345
- const bundle = teleportPack(secrets, passphrase);
2346
- process.stdout.write(bundle);
2347
- if (process.stdout.isTTY) {
2216
+ const results = lintFiles(files, {
2217
+ fix: cmd.fix,
2218
+ scope: opts.scope,
2219
+ projectPath: opts.projectPath
2220
+ });
2221
+ if (results.length === 0) {
2348
2222
  console.log(
2349
2223
  `
2350
- ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
2224
+ ${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
2225
+ `
2351
2226
  );
2227
+ return;
2352
2228
  }
2353
- });
2354
- tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").action(async (bundle, cmd) => {
2355
- if (!bundle) {
2356
- const chunks = [];
2357
- for await (const chunk of process.stdin) {
2358
- chunks.push(chunk);
2359
- }
2360
- bundle = Buffer.concat(chunks).toString("utf8").trim();
2229
+ for (const res of results) {
2230
+ const status = res.fixed ? c.green("fixed") : c.red("found");
2231
+ console.log(
2232
+ ` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`
2233
+ );
2234
+ console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
2235
+ console.log(
2236
+ ` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`
2237
+ );
2238
+ console.log();
2361
2239
  }
2362
- const passphrase = await promptSecret(
2363
- `${SYMBOLS.lock} Enter passphrase for decryption: `
2364
- );
2365
- try {
2366
- const payload = teleportUnpack(bundle, passphrase);
2367
- if (cmd.dryRun) {
2368
- console.log(
2369
- `
2370
- ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
2371
- `
2372
- );
2373
- for (const s of payload.secrets) {
2374
- console.log(` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`);
2375
- }
2376
- return;
2377
- }
2378
- const opts = buildOpts(cmd);
2379
- for (const s of payload.secrets) {
2380
- setSecret(s.key, s.value, opts);
2381
- }
2240
+ const fixedCount = results.filter((r) => r.fixed).length;
2241
+ if (cmd.fix && fixedCount > 0) {
2382
2242
  console.log(
2383
- `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
2243
+ ` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
2244
+ `
2384
2245
  );
2385
- } catch {
2386
- console.error(
2387
- c.red(`${SYMBOLS.cross} Failed to unpack: wrong passphrase or corrupted bundle`)
2246
+ } else {
2247
+ console.log(
2248
+ ` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
2249
+ `
2388
2250
  );
2389
- process.exit(1);
2390
2251
  }
2391
2252
  });
2253
+ 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) => {
2254
+ const { startDashboardServer } = await import("./dashboard-OXUD62RV.js");
2255
+ const { exec } = await import("child_process");
2256
+ const { platform } = await import("os");
2257
+ const port = Number(cmd.port);
2258
+ const { close } = startDashboardServer({ port });
2259
+ const url = `http://127.0.0.1:${port}`;
2260
+ console.log(
2261
+ `
2262
+ ${SYMBOLS.zap} ${c.bold("q-ring quantum status dashboard")}
2263
+ `
2264
+ );
2265
+ console.log(` ${c.cyan(url)}
2266
+ `);
2267
+ console.log(c.dim(" Press Ctrl+C to stop\n"));
2268
+ if (cmd.open !== false) {
2269
+ const openCmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2270
+ exec(`${openCmd} ${url}`);
2271
+ }
2272
+ const shutdown = () => {
2273
+ console.log(`
2274
+ ${c.dim(" dashboard stopped")}`);
2275
+ close();
2276
+ process.exit(0);
2277
+ };
2278
+ process.on("SIGINT", shutdown);
2279
+ process.on("SIGTERM", shutdown);
2280
+ await new Promise(() => {
2281
+ });
2282
+ });
2283
+ }
2284
+
2285
+ // src/cli/commands/audit.ts
2286
+ import { writeFileSync as writeFileSync3 } from "fs";
2287
+ function registerAuditCommands(program2) {
2392
2288
  program2.command("audit").description("View the audit log (observer effect)").option("-k, --key <key>", "Filter by key").option(
2393
2289
  "-a, --action <action>",
2394
2290
  "Filter by action (read, write, delete, etc.)"
@@ -2396,9 +2292,7 @@ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets)
2396
2292
  if (cmd.anomalies) {
2397
2293
  const anomalies = detectAnomalies(cmd.key);
2398
2294
  if (anomalies.length === 0) {
2399
- console.log(
2400
- `${SYMBOLS.shield} ${c.green("No anomalies detected")}`
2401
- );
2295
+ console.log(`${SYMBOLS.shield} ${c.green("No anomalies detected")}`);
2402
2296
  return;
2403
2297
  }
2404
2298
  console.log(
@@ -2447,12 +2341,24 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2447
2341
  return;
2448
2342
  }
2449
2343
  if (result.intact) {
2450
- console.log(`${SYMBOLS.shield} ${c.green("Audit chain intact")} \u2014 ${result.totalEvents} events verified`);
2344
+ console.log(
2345
+ `${SYMBOLS.shield} ${c.green("Audit chain intact")} \u2014 ${result.totalEvents} events verified`
2346
+ );
2451
2347
  } else {
2452
- console.log(`${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`);
2453
- console.log(c.dim(` ${result.validEvents}/${result.totalEvents} events valid before break`));
2348
+ console.log(
2349
+ `${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`
2350
+ );
2351
+ console.log(
2352
+ c.dim(
2353
+ ` ${result.validEvents}/${result.totalEvents} events valid before break`
2354
+ )
2355
+ );
2454
2356
  if (result.brokenEvent) {
2455
- console.log(c.dim(` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`));
2357
+ console.log(
2358
+ c.dim(
2359
+ ` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`
2360
+ )
2361
+ );
2456
2362
  }
2457
2363
  process.exitCode = 1;
2458
2364
  }
@@ -2464,7 +2370,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2464
2370
  format: cmd.format
2465
2371
  });
2466
2372
  if (cmd.output) {
2467
- writeFileSync4(cmd.output, output);
2373
+ writeFileSync3(cmd.output, output);
2468
2374
  console.log(`${SYMBOLS.check} Exported to ${cmd.output}`);
2469
2375
  } else {
2470
2376
  console.log(output);
@@ -2477,11 +2383,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2477
2383
  console.log(c.dim("No secrets to check"));
2478
2384
  return;
2479
2385
  }
2480
- console.log(
2481
- c.bold(`
2386
+ console.log(c.bold(`
2482
2387
  ${SYMBOLS.shield} Secret health report
2483
- `)
2484
- );
2388
+ `));
2485
2389
  let healthy = 0;
2486
2390
  let stale = 0;
2487
2391
  let expired = 0;
@@ -2505,31 +2409,147 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2505
2409
  healthy++;
2506
2410
  }
2507
2411
  }
2508
- console.log(
2509
- `
2510
- ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
2511
- );
2512
- const anomalies = detectAnomalies();
2513
- if (anomalies.length > 0) {
2412
+ console.log(
2413
+ `
2414
+ ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
2415
+ );
2416
+ const anomalies = detectAnomalies();
2417
+ if (anomalies.length > 0) {
2418
+ console.log(
2419
+ `
2420
+ ${c.yellow(`${SYMBOLS.warning} ${anomalies.length} access anomaly/anomalies detected`)} ${c.dim("(run: qring audit --anomalies)")}`
2421
+ );
2422
+ }
2423
+ console.log();
2424
+ });
2425
+ }
2426
+
2427
+ // src/hooks/precommit.ts
2428
+ import { execSync } from "child_process";
2429
+ import { existsSync as existsSync3, writeFileSync as writeFileSync4, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
2430
+ import { join as join2 } from "path";
2431
+ function getStagedFiles() {
2432
+ try {
2433
+ const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
2434
+ encoding: "utf8",
2435
+ stdio: ["pipe", "pipe", "pipe"]
2436
+ });
2437
+ return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
2438
+ } catch {
2439
+ return [];
2440
+ }
2441
+ }
2442
+ function runPreCommitScan() {
2443
+ const staged = getStagedFiles();
2444
+ if (staged.length === 0) return 0;
2445
+ const results = lintFiles(staged);
2446
+ if (results.length === 0) return 0;
2447
+ console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
2448
+ for (const r of results) {
2449
+ console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
2450
+ }
2451
+ console.error(
2452
+ `
2453
+ [q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
2454
+ `
2455
+ );
2456
+ return 1;
2457
+ }
2458
+ var HOOK_SCRIPT = `#!/bin/sh
2459
+ # q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
2460
+ npx qring hook:run 2>&1
2461
+ exit $?
2462
+ `;
2463
+ function installPreCommitHook(repoPath) {
2464
+ const root = repoPath ?? process.cwd();
2465
+ const hooksDir = join2(root, ".git", "hooks");
2466
+ if (!existsSync3(join2(root, ".git"))) {
2467
+ return { installed: false, path: "", message: "Not a git repository" };
2468
+ }
2469
+ const hookPath = join2(hooksDir, "pre-commit");
2470
+ if (existsSync3(hookPath)) {
2471
+ const existing = readFileSync5(hookPath, "utf8");
2472
+ if (existing.includes("q-ring")) {
2473
+ return { installed: true, path: hookPath, message: "Hook already installed" };
2474
+ }
2475
+ writeFileSync4(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
2476
+ } else {
2477
+ writeFileSync4(hookPath, HOOK_SCRIPT, "utf8");
2478
+ }
2479
+ chmodSync(hookPath, 493);
2480
+ return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
2481
+ }
2482
+ function uninstallPreCommitHook(repoPath) {
2483
+ const root = repoPath ?? process.cwd();
2484
+ const hookPath = join2(root, ".git", "hooks", "pre-commit");
2485
+ if (!existsSync3(hookPath)) return false;
2486
+ const content = readFileSync5(hookPath, "utf8");
2487
+ if (!content.includes("q-ring")) return false;
2488
+ const lines = content.split("\n");
2489
+ const cleaned = lines.filter(
2490
+ (l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
2491
+ );
2492
+ if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
2493
+ unlinkSync(hookPath);
2494
+ } else {
2495
+ writeFileSync4(hookPath, cleaned.join("\n"), "utf8");
2496
+ }
2497
+ return true;
2498
+ }
2499
+
2500
+ // src/cli/commands/hooks.ts
2501
+ function registerHookCommands(program2) {
2502
+ program2.command("hook:install").description(
2503
+ "Install a git pre-commit hook that scans for hardcoded secrets"
2504
+ ).option("--project-path <path>", "Repository path").action((cmd) => {
2505
+ const result = installPreCommitHook(cmd.projectPath);
2506
+ if (result.installed) {
2514
2507
  console.log(
2515
- `
2516
- ${c.yellow(`${SYMBOLS.warning} ${anomalies.length} access anomaly/anomalies detected`)} ${c.dim("(run: qring audit --anomalies)")}`
2508
+ `${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`
2517
2509
  );
2510
+ } else {
2511
+ console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
2518
2512
  }
2519
- console.log();
2520
2513
  });
2521
- const hook = program2.command("hook").description("Manage secret change hooks (callbacks on write/delete/rotate)");
2522
- 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) => {
2514
+ program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
2515
+ const removed = uninstallPreCommitHook(cmd.projectPath);
2516
+ if (removed) {
2517
+ console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
2518
+ } else {
2519
+ console.log(c.dim("No q-ring pre-commit hook found"));
2520
+ }
2521
+ });
2522
+ program2.command("hook:run").description("Run the pre-commit secret scan (called by the git hook)").action(() => {
2523
+ const code = runPreCommitScan();
2524
+ process.exit(code);
2525
+ });
2526
+ const hook = program2.command("hook").description(
2527
+ "Manage secret change hooks (callbacks on write/delete/rotate)"
2528
+ );
2529
+ 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(
2530
+ "--scope <scope>",
2531
+ "Trigger only for this scope (global or project)"
2532
+ ).option(
2533
+ "--action <actions>",
2534
+ "Comma-separated actions: write,delete,rotate",
2535
+ "write,delete,rotate"
2536
+ ).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) => {
2523
2537
  let type;
2524
2538
  if (cmd.exec) type = "shell";
2525
2539
  else if (cmd.url) type = "http";
2526
2540
  else if (cmd.signalTarget) type = "signal";
2527
2541
  else {
2528
- console.error(c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`));
2542
+ console.error(
2543
+ c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`)
2544
+ );
2529
2545
  process.exit(1);
2530
2546
  }
2531
2547
  if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
2532
- console.error(c.red(`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`));
2548
+ console.error(
2549
+ c.red(
2550
+ `${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`
2551
+ )
2552
+ );
2533
2553
  process.exit(1);
2534
2554
  }
2535
2555
  const actions = cmd.action.split(",").map((a) => a.trim());
@@ -2548,7 +2568,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2548
2568
  description: cmd.description,
2549
2569
  enabled: true
2550
2570
  });
2551
- console.log(`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`);
2571
+ console.log(
2572
+ `${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`
2573
+ );
2552
2574
  if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
2553
2575
  if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
2554
2576
  if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
@@ -2559,9 +2581,11 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2559
2581
  console.log(c.dim("No hooks registered"));
2560
2582
  return;
2561
2583
  }
2562
- console.log(c.bold(`
2584
+ console.log(
2585
+ c.bold(`
2563
2586
  ${SYMBOLS.zap} Registered hooks (${hooks.length})
2564
- `));
2587
+ `)
2588
+ );
2565
2589
  for (const h of hooks) {
2566
2590
  const status = h.enabled ? c.green("on") : c.red("off");
2567
2591
  const matchParts = [];
@@ -2569,10 +2593,15 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2569
2593
  if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
2570
2594
  if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
2571
2595
  if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
2572
- if (h.match.action?.length) matchParts.push(`actions=${h.match.action.join(",")}`);
2596
+ if (h.match.action?.length)
2597
+ matchParts.push(`actions=${h.match.action.join(",")}`);
2573
2598
  const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
2574
- console.log(` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`);
2575
- console.log(` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`);
2599
+ console.log(
2600
+ ` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`
2601
+ );
2602
+ console.log(
2603
+ ` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`
2604
+ );
2576
2605
  }
2577
2606
  console.log();
2578
2607
  });
@@ -2594,7 +2623,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2594
2623
  });
2595
2624
  hook.command("disable <id>").description("Disable a hook").action((id) => {
2596
2625
  if (disableHook(id)) {
2597
- console.log(`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`);
2626
+ console.log(
2627
+ `${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`
2628
+ );
2598
2629
  } else {
2599
2630
  console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
2600
2631
  process.exit(1);
@@ -2622,96 +2653,302 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2622
2653
  const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
2623
2654
  console.log(` ${icon} ${result.message}`);
2624
2655
  } else {
2625
- console.log(c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`));
2656
+ console.log(
2657
+ c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`)
2658
+ );
2626
2659
  }
2627
2660
  console.log();
2628
2661
  });
2629
- 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) => {
2630
- const projectPath = cmd.projectPath ?? process.cwd();
2631
- const config = readProjectConfig(projectPath);
2632
- if (!config?.secrets || Object.keys(config.secrets).length === 0) {
2633
- console.error(
2634
- c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
2662
+ }
2663
+
2664
+ // src/core/agent.ts
2665
+ function defaultConfig() {
2666
+ return {
2667
+ intervalSeconds: 60,
2668
+ autoRotate: false,
2669
+ projectPaths: [process.cwd()],
2670
+ verbose: false
2671
+ };
2672
+ }
2673
+ function runHealthScan(config = {}) {
2674
+ const cfg = { ...defaultConfig(), ...config };
2675
+ const report = {
2676
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2677
+ totalSecrets: 0,
2678
+ healthy: 0,
2679
+ stale: 0,
2680
+ expired: 0,
2681
+ anomalies: 0,
2682
+ rotated: [],
2683
+ warnings: []
2684
+ };
2685
+ const globalEntries = listSecrets({ scope: "global", source: "agent" });
2686
+ const projectEntries = cfg.projectPaths.flatMap(
2687
+ (pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
2688
+ );
2689
+ const allEntries = [...globalEntries, ...projectEntries];
2690
+ report.totalSecrets = allEntries.length;
2691
+ for (const entry of allEntries) {
2692
+ if (!entry.envelope) continue;
2693
+ const decay = checkDecay(entry.envelope);
2694
+ if (decay.isExpired) {
2695
+ report.expired++;
2696
+ report.warnings.push(
2697
+ `EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
2635
2698
  );
2636
- process.exit(1);
2699
+ if (cfg.autoRotate) {
2700
+ const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
2701
+ const prefix = entry.envelope?.meta.rotationPrefix;
2702
+ const newValue = generateSecret({ format: fmt, prefix });
2703
+ setSecret(entry.key, newValue, {
2704
+ scope: entry.scope,
2705
+ projectPath: cfg.projectPaths[0],
2706
+ source: "agent"
2707
+ });
2708
+ report.rotated.push(entry.key);
2709
+ logAudit({
2710
+ action: "write",
2711
+ key: entry.key,
2712
+ scope: entry.scope,
2713
+ source: "agent",
2714
+ detail: "auto-rotated by agent (expired)"
2715
+ });
2716
+ fireHooks({
2717
+ action: "rotate",
2718
+ key: entry.key,
2719
+ scope: entry.scope,
2720
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2721
+ source: "agent"
2722
+ }, entry.envelope?.meta.tags).catch(() => {
2723
+ });
2724
+ }
2725
+ } else if (decay.isStale) {
2726
+ report.stale++;
2727
+ report.warnings.push(
2728
+ `STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
2729
+ );
2730
+ } else {
2731
+ report.healthy++;
2732
+ }
2733
+ }
2734
+ const anomalies = detectAnomalies();
2735
+ report.anomalies = anomalies.length;
2736
+ for (const a of anomalies) {
2737
+ report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
2738
+ }
2739
+ return report;
2740
+ }
2741
+ function formatReport(report, verbose) {
2742
+ const lines = [];
2743
+ lines.push(
2744
+ `${c.bold(`${SYMBOLS.shield} q-ring agent scan`)} ${c.dim(report.timestamp)}`
2745
+ );
2746
+ lines.push(
2747
+ ` ${c.dim("secrets:")} ${report.totalSecrets} ${c.green(`${SYMBOLS.check} ${report.healthy}`)} ${c.yellow(`${SYMBOLS.warning} ${report.stale}`)} ${c.red(`${SYMBOLS.cross} ${report.expired}`)} ${c.dim(`anomalies: ${report.anomalies}`)}`
2748
+ );
2749
+ if (report.rotated.length > 0) {
2750
+ lines.push(
2751
+ ` ${c.cyan(`${SYMBOLS.zap} auto-rotated:`)} ${report.rotated.join(", ")}`
2752
+ );
2753
+ }
2754
+ if (verbose && report.warnings.length > 0) {
2755
+ lines.push("");
2756
+ for (const w of report.warnings) {
2757
+ if (w.startsWith("EXPIRED")) lines.push(` ${c.red(w)}`);
2758
+ else if (w.startsWith("STALE")) lines.push(` ${c.yellow(w)}`);
2759
+ else if (w.startsWith("ANOMALY")) lines.push(` ${c.magenta(w)}`);
2760
+ else lines.push(` ${w}`);
2761
+ }
2762
+ }
2763
+ return lines.join("\n");
2764
+ }
2765
+ async function startAgent(config = {}) {
2766
+ const cfg = { ...defaultConfig(), ...config };
2767
+ console.log(
2768
+ `${c.bold(`${SYMBOLS.zap} q-ring agent started`)} ${c.dim(`(interval: ${cfg.intervalSeconds}s, auto-rotate: ${cfg.autoRotate})`)}`
2769
+ );
2770
+ console.log(
2771
+ c.dim(` monitoring: global + ${cfg.projectPaths.length} project(s)`)
2772
+ );
2773
+ console.log();
2774
+ const scan = () => {
2775
+ const report = runHealthScan(cfg);
2776
+ console.log(formatReport(report, cfg.verbose));
2777
+ if (report.warnings.length > 0 || cfg.verbose) {
2778
+ console.log();
2779
+ }
2780
+ };
2781
+ scan();
2782
+ const interval = setInterval(scan, cfg.intervalSeconds * 1e3);
2783
+ const shutdown = () => {
2784
+ clearInterval(interval);
2785
+ console.log(`
2786
+ ${c.dim("q-ring agent stopped")}`);
2787
+ process.exit(0);
2788
+ };
2789
+ process.on("SIGINT", shutdown);
2790
+ process.on("SIGTERM", shutdown);
2791
+ await new Promise(() => {
2792
+ });
2793
+ }
2794
+
2795
+ // src/cli/commands/agent.ts
2796
+ function registerAgentCommands(program2) {
2797
+ program2.command("remember <key> <value>").description(
2798
+ "Store a key-value pair in encrypted agent memory (persists across sessions)"
2799
+ ).action((key, value) => {
2800
+ remember(key, value);
2801
+ console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
2802
+ });
2803
+ program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
2804
+ if (!key) {
2805
+ const entries = listMemory();
2806
+ if (entries.length === 0) {
2807
+ console.log(c.dim("Agent memory is empty."));
2808
+ return;
2809
+ }
2810
+ console.log(
2811
+ `
2812
+ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2813
+ `
2814
+ );
2815
+ for (const e of entries) {
2816
+ console.log(
2817
+ ` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`
2818
+ );
2819
+ }
2820
+ console.log();
2821
+ return;
2822
+ }
2823
+ const value = recall(key);
2824
+ if (value === null) {
2825
+ console.log(c.dim(`No memory found for "${key}"`));
2826
+ } else {
2827
+ console.log(safeStr(value));
2828
+ }
2829
+ });
2830
+ program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
2831
+ if (cmd.all) {
2832
+ clearMemory();
2833
+ console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
2834
+ return;
2835
+ }
2836
+ const removed = forget(key);
2837
+ if (removed) {
2838
+ console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
2839
+ } else {
2840
+ console.log(c.dim(`No memory found for "${key}"`));
2637
2841
  }
2842
+ });
2843
+ program2.command("analyze").description(
2844
+ "Analyze secret usage patterns and provide optimization suggestions"
2845
+ ).option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
2638
2846
  const opts = buildOpts(cmd);
2639
- const lines = [];
2640
- const warnings = [];
2641
- for (const [key, manifest] of Object.entries(config.secrets)) {
2642
- const value = getSecret(key, { ...opts, projectPath, source: "cli" });
2643
- if (value === null) {
2644
- if (manifest.required !== false) {
2645
- warnings.push(`MISSING (required): ${key}`);
2646
- }
2647
- lines.push(`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`);
2648
- continue;
2847
+ const entries = listSecrets({ ...opts, silent: true });
2848
+ const audit = queryAudit({ limit: 1e3 });
2849
+ console.log(`
2850
+ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2851
+ `);
2852
+ const accessMap = /* @__PURE__ */ new Map();
2853
+ for (const e of audit) {
2854
+ if (e.action === "read" && e.key) {
2855
+ accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
2649
2856
  }
2650
- const result = getEnvelope(key, { projectPath, source: "cli" });
2651
- if (result) {
2652
- const decay = checkDecay(result.envelope);
2653
- if (decay.isExpired) {
2654
- warnings.push(`EXPIRED: ${key}`);
2655
- } else if (decay.isStale) {
2656
- warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
2657
- }
2857
+ }
2858
+ const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
2859
+ if (sorted.length > 0) {
2860
+ console.log(` ${c.bold("Most accessed:")}`);
2861
+ for (const [key, count] of sorted.slice(0, 5)) {
2862
+ console.log(` ${c.bold(key)} \u2014 ${c.cyan(count.toString())} reads`);
2658
2863
  }
2659
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
2660
- lines.push(`${key}="${escaped}"`);
2864
+ console.log();
2661
2865
  }
2662
- const output = lines.join("\n") + "\n";
2663
- if (cmd.output) {
2664
- writeFileSync4(cmd.output, output);
2866
+ const neverAccessed = entries.filter((e) => {
2867
+ const count = e.envelope?.meta.accessCount ?? 0;
2868
+ return count === 0;
2869
+ });
2870
+ if (neverAccessed.length > 0) {
2665
2871
  console.log(
2666
- `${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
2872
+ ` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`
2667
2873
  );
2668
- } else {
2669
- process.stdout.write(output);
2874
+ for (const e of neverAccessed.slice(0, 8)) {
2875
+ const age = e.envelope?.meta.createdAt ? c.dim(
2876
+ `(created ${new Date(e.envelope.meta.createdAt).toLocaleDateString()})`
2877
+ ) : "";
2878
+ console.log(` ${c.dim(SYMBOLS.cross)} ${e.key} ${age}`);
2879
+ }
2880
+ console.log();
2670
2881
  }
2671
- if (warnings.length > 0 && process.stderr.isTTY) {
2672
- console.error();
2673
- for (const w of warnings) {
2674
- console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
2882
+ const expired = entries.filter((e) => e.decay?.isExpired);
2883
+ const stale = entries.filter(
2884
+ (e) => e.decay?.isStale && !e.decay?.isExpired
2885
+ );
2886
+ if (expired.length > 0) {
2887
+ console.log(
2888
+ ` ${c.red("Expired:")} ${expired.length} secrets need rotation or cleanup`
2889
+ );
2890
+ for (const e of expired.slice(0, 5)) {
2891
+ console.log(` ${c.red(SYMBOLS.cross)} ${e.key}`);
2675
2892
  }
2676
- console.error();
2893
+ console.log();
2677
2894
  }
2678
- });
2679
- 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) => {
2680
- const { startDashboardServer } = await import("./dashboard-5L4ILERJ.js");
2681
- const { exec } = await import("child_process");
2682
- const { platform } = await import("os");
2683
- const port = Number(cmd.port);
2684
- const { close } = startDashboardServer({ port });
2685
- const url = `http://127.0.0.1:${port}`;
2686
- console.log(
2687
- `
2688
- ${SYMBOLS.zap} ${c.bold("q-ring quantum status dashboard")}
2689
- `
2895
+ if (stale.length > 0) {
2896
+ console.log(
2897
+ ` ${c.yellow("Stale (>75% lifetime):")} ${stale.length} secrets approaching expiry`
2898
+ );
2899
+ for (const e of stale.slice(0, 5)) {
2900
+ console.log(
2901
+ ` ${c.yellow(SYMBOLS.warning)} ${e.key} ${c.dim(`(${e.decay?.timeRemaining} remaining)`)}`
2902
+ );
2903
+ }
2904
+ console.log();
2905
+ }
2906
+ const globalOnly = entries.filter((e) => e.scope === "global");
2907
+ const withProjectTags = globalOnly.filter(
2908
+ (e) => e.envelope?.meta.tags?.some(
2909
+ (t) => ["backend", "frontend", "db", "api"].includes(t)
2910
+ )
2690
2911
  );
2691
- console.log(` ${c.cyan(url)}
2692
- `);
2693
- console.log(c.dim(" Press Ctrl+C to stop\n"));
2694
- if (cmd.open !== false) {
2695
- const openCmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2696
- exec(`${openCmd} ${url}`);
2912
+ if (withProjectTags.length > 0) {
2913
+ console.log(` ${c.bold("Scope suggestions:")}`);
2914
+ console.log(
2915
+ ` ${withProjectTags.length} global secret(s) have project-specific tags \u2014 consider moving to project scope`
2916
+ );
2917
+ console.log();
2697
2918
  }
2698
- const shutdown = () => {
2699
- console.log(`
2700
- ${c.dim(" dashboard stopped")}`);
2701
- close();
2702
- process.exit(0);
2703
- };
2704
- process.on("SIGINT", shutdown);
2705
- process.on("SIGTERM", shutdown);
2706
- await new Promise(() => {
2707
- });
2919
+ const noRotation = entries.filter(
2920
+ (e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
2921
+ );
2922
+ if (noRotation.length > 0) {
2923
+ console.log(` ${c.bold("Rotation suggestions:")}`);
2924
+ console.log(
2925
+ ` ${noRotation.length} secret(s) have no rotation format set`
2926
+ );
2927
+ console.log(
2928
+ ` Use ${c.bold("qring set <key> <value> --rotation-format api-key")} to enable auto-rotation`
2929
+ );
2930
+ console.log();
2931
+ }
2932
+ console.log(` ${c.bold("Summary:")}`);
2933
+ console.log(` Total secrets: ${entries.length}`);
2934
+ console.log(` Active: ${entries.length - expired.length}`);
2935
+ console.log(` Expired: ${expired.length}`);
2936
+ console.log(` Stale: ${stale.length}`);
2937
+ console.log(` Never accessed: ${neverAccessed.length}`);
2938
+ console.log(
2939
+ ` With rotation config: ${entries.length - noRotation.length}`
2940
+ );
2941
+ console.log();
2708
2942
  });
2709
2943
  program2.command("agent").description("Start the autonomous agent (background monitor)").option(
2710
2944
  "-i, --interval <seconds>",
2711
2945
  "Scan interval in seconds",
2712
2946
  parseInt,
2713
2947
  60
2714
- ).option("--auto-rotate", "Auto-rotate expired secrets").option("-v, --verbose", "Verbose output with all warnings").option("--project-path <paths>", "Comma-separated project paths to monitor").option("--once", "Run a single scan and exit (no daemon)").action(async (cmd) => {
2948
+ ).option("--auto-rotate", "Auto-rotate expired secrets").option("-v, --verbose", "Verbose output with all warnings").option(
2949
+ "--project-path <paths>",
2950
+ "Comma-separated project paths to monitor"
2951
+ ).option("--once", "Run a single scan and exit (no daemon)").action(async (cmd) => {
2715
2952
  const projectPaths = cmd.projectPath ? cmd.projectPath.split(",").map((p) => p.trim()) : [process.cwd()];
2716
2953
  if (cmd.once) {
2717
2954
  const report = runHealthScan({
@@ -2729,96 +2966,321 @@ ${c.dim(" dashboard stopped")}`);
2729
2966
  verbose: cmd.verbose
2730
2967
  });
2731
2968
  });
2732
- 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) => {
2969
+ }
2970
+
2971
+ // src/cli/commands/security.ts
2972
+ function registerSecurityCommands(program2) {
2973
+ program2.command("approve <key>").description(
2974
+ "Grant a scoped, reasoned, HMAC-verified approval token for MCP secret access"
2975
+ ).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) => {
2733
2976
  const opts = buildOpts(cmd);
2734
- const value = getSecret(key, opts);
2735
- if (!value) {
2736
- console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
2737
- process.exitCode = 1;
2977
+ const scope = opts.scope ?? "global";
2978
+ if (cmd.list) {
2979
+ const approvals = listApprovals();
2980
+ if (approvals.length === 0) {
2981
+ console.log(c.dim(" No active approvals"));
2982
+ return;
2983
+ }
2984
+ for (const a of approvals) {
2985
+ const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
2986
+ const ttl = Math.max(
2987
+ 0,
2988
+ Math.round(
2989
+ (new Date(a.expiresAt).getTime() - Date.now()) / 1e3
2990
+ )
2991
+ );
2992
+ console.log(
2993
+ ` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`
2994
+ );
2995
+ }
2738
2996
  return;
2739
2997
  }
2740
- const result = await rotateWithProvider(value, cmd.provider);
2741
- if (result.rotated && result.newValue) {
2742
- setSecret(key, result.newValue, { ...opts, scope: opts.scope ?? "global" });
2743
- console.log(`${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`);
2744
- console.log(c.dim(` ${result.message}`));
2745
- } else {
2746
- console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
2998
+ if (cmd.revoke) {
2999
+ const revoked = revokeApproval(key, scope);
3000
+ if (revoked) {
3001
+ console.log(
3002
+ `${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`
3003
+ );
3004
+ } else {
3005
+ console.log(c.dim(` No active approval found for ${key}`));
3006
+ }
3007
+ return;
2747
3008
  }
3009
+ const entry = grantApproval(key, scope, cmd.for, {
3010
+ reason: cmd.reason ?? "manual approval"
3011
+ });
3012
+ console.log(
3013
+ `${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`
3014
+ );
3015
+ console.log(
3016
+ c.dim(
3017
+ ` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`
3018
+ )
3019
+ );
2748
3020
  });
2749
- 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) => {
2750
- const opts = buildOpts(cmd);
2751
- const entries = listSecrets(opts);
2752
- const secrets = entries.map((e) => {
2753
- const val = getSecret(e.key, { ...opts, scope: e.scope, silent: true });
2754
- if (!val) return null;
2755
- const provider = e.envelope?.meta.provider;
2756
- const validationUrl = e.envelope?.meta.validationUrl;
2757
- return { key: e.key, value: val, provider, validationUrl };
2758
- }).filter((s) => s !== null);
2759
- if (secrets.length === 0) {
2760
- console.log(c.dim("No secrets to validate"));
3021
+ program2.command("approvals").description("List all approval tokens with verification status").action(() => {
3022
+ const approvals = listApprovals();
3023
+ if (approvals.length === 0) {
3024
+ console.log(c.dim(" No approvals found"));
2761
3025
  return;
2762
3026
  }
2763
- const report = await ciValidateBatch(secrets);
2764
- if (cmd.json) {
2765
- console.log(JSON.stringify(report, null, 2));
2766
- } else {
2767
- console.log(c.bold(`
2768
- CI Secret Validation: ${report.results.length} secrets
3027
+ console.log(c.bold(`
3028
+ ${SYMBOLS.lock} Approval Tokens
2769
3029
  `));
2770
- for (const r of report.results) {
2771
- const icon = r.validation.valid ? SYMBOLS.check : SYMBOLS.cross;
2772
- const color = r.validation.valid ? c.green : c.red;
2773
- console.log(` ${icon} ${color(r.key)} [${r.validation.provider}] ${r.validation.message}`);
2774
- if (r.requiresRotation) console.log(c.dim(` \u2192 rotation required`));
2775
- }
2776
- console.log();
2777
- if (!report.allValid) {
2778
- console.log(c.red(` ${report.failCount} secret(s) failed validation`));
2779
- process.exitCode = 1;
2780
- } else {
2781
- console.log(c.green(` All secrets valid`));
2782
- }
3030
+ for (const a of approvals) {
3031
+ const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
3032
+ const ttl = Math.max(
3033
+ 0,
3034
+ Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3)
3035
+ );
3036
+ console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
3037
+ console.log(
3038
+ c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`)
3039
+ );
3040
+ if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
2783
3041
  }
3042
+ console.log();
2784
3043
  });
2785
3044
  program2.command("policy").description("Show project governance policy summary").option("--json", "Output as JSON").action((cmd) => {
2786
3045
  const summary = getPolicySummary();
2787
- if (cmd.json) {
3046
+ if (wantsJsonOutput(program2, cmd)) {
2788
3047
  console.log(JSON.stringify(summary, null, 2));
2789
3048
  return;
2790
3049
  }
2791
3050
  console.log(c.bold("\n\u2696 Governance Policy\n"));
2792
3051
  if (!summary.hasMcpPolicy && !summary.hasExecPolicy && !summary.hasSecretPolicy) {
2793
3052
  console.log(c.dim(" No policy configured in .q-ring.json"));
2794
- console.log(c.dim(' Add a "policy" section to enable governance controls.\n'));
3053
+ console.log(
3054
+ c.dim(' Add a "policy" section to enable governance controls.\n')
3055
+ );
2795
3056
  return;
2796
3057
  }
2797
3058
  if (summary.hasMcpPolicy) {
2798
3059
  console.log(c.cyan(" MCP Policy:"));
2799
3060
  const m = summary.details.mcp;
2800
- if (m.allowTools) console.log(c.green(` Allow tools: ${m.allowTools.join(", ")}`));
2801
- if (m.denyTools) console.log(c.red(` Deny tools: ${m.denyTools.join(", ")}`));
2802
- if (m.readableKeys) console.log(c.green(` Readable keys: ${m.readableKeys.join(", ")}`));
2803
- if (m.deniedKeys) console.log(c.red(` Denied keys: ${m.deniedKeys.join(", ")}`));
2804
- if (m.deniedTags) console.log(c.red(` Denied tags: ${m.deniedTags.join(", ")}`));
3061
+ if (m.allowTools)
3062
+ console.log(c.green(` Allow tools: ${m.allowTools.join(", ")}`));
3063
+ if (m.denyTools)
3064
+ console.log(c.red(` Deny tools: ${m.denyTools.join(", ")}`));
3065
+ if (m.readableKeys)
3066
+ console.log(
3067
+ c.green(` Readable keys: ${m.readableKeys.join(", ")}`)
3068
+ );
3069
+ if (m.deniedKeys)
3070
+ console.log(c.red(` Denied keys: ${m.deniedKeys.join(", ")}`));
3071
+ if (m.deniedTags)
3072
+ console.log(c.red(` Denied tags: ${m.deniedTags.join(", ")}`));
2805
3073
  }
2806
3074
  if (summary.hasExecPolicy) {
2807
3075
  console.log(c.cyan(" Exec Policy:"));
2808
3076
  const e = summary.details.exec;
2809
- if (e.allowCommands) console.log(c.green(` Allow commands: ${e.allowCommands.join(", ")}`));
2810
- if (e.denyCommands) console.log(c.red(` Deny commands: ${e.denyCommands.join(", ")}`));
2811
- if (e.maxRuntimeSeconds) console.log(` Max runtime: ${e.maxRuntimeSeconds}s`);
2812
- if (e.allowNetwork !== void 0) console.log(` Allow network: ${e.allowNetwork}`);
3077
+ if (e.allowCommands)
3078
+ console.log(
3079
+ c.green(` Allow commands: ${e.allowCommands.join(", ")}`)
3080
+ );
3081
+ if (e.denyCommands)
3082
+ console.log(
3083
+ c.red(` Deny commands: ${e.denyCommands.join(", ")}`)
3084
+ );
3085
+ if (e.maxRuntimeSeconds)
3086
+ console.log(` Max runtime: ${e.maxRuntimeSeconds}s`);
3087
+ if (e.allowNetwork !== void 0)
3088
+ console.log(` Allow network: ${e.allowNetwork}`);
2813
3089
  }
2814
3090
  if (summary.hasSecretPolicy) {
2815
3091
  console.log(c.cyan(" Secret Lifecycle Policy:"));
2816
3092
  const s = summary.details.secrets;
2817
- if (s.requireApprovalForTags) console.log(` Require approval for tags: ${s.requireApprovalForTags.join(", ")}`);
3093
+ if (s.requireApprovalForTags)
3094
+ console.log(
3095
+ ` Require approval for tags: ${s.requireApprovalForTags.join(", ")}`
3096
+ );
2818
3097
  if (s.maxTtlSeconds) console.log(` Max TTL: ${s.maxTtlSeconds}s`);
2819
3098
  }
2820
3099
  console.log();
2821
3100
  });
3101
+ }
3102
+
3103
+ // src/cli/commands.ts
3104
+ var COMMAND_GROUPS = [
3105
+ {
3106
+ name: "Secrets",
3107
+ symbol: SYMBOLS.key,
3108
+ commands: [
3109
+ "set",
3110
+ "get",
3111
+ "delete",
3112
+ "list",
3113
+ "inspect",
3114
+ "export",
3115
+ "import"
3116
+ ]
3117
+ },
3118
+ {
3119
+ name: "Project",
3120
+ symbol: SYMBOLS.package,
3121
+ commands: ["context", "check", "env", "env:generate", "wizard"]
3122
+ },
3123
+ {
3124
+ name: "Quantum",
3125
+ symbol: SYMBOLS.sparkle,
3126
+ commands: [
3127
+ "generate",
3128
+ "entangle",
3129
+ "disentangle",
3130
+ "tunnel",
3131
+ "teleport"
3132
+ ]
3133
+ },
3134
+ {
3135
+ name: "Validation & Rotation",
3136
+ symbol: SYMBOLS.shield,
3137
+ commands: ["validate", "rotate", "ci:validate"]
3138
+ },
3139
+ {
3140
+ name: "Dev Tooling",
3141
+ symbol: SYMBOLS.zap,
3142
+ commands: ["exec", "scan", "lint", "status"]
3143
+ },
3144
+ {
3145
+ name: "Audit & Health",
3146
+ symbol: SYMBOLS.eye,
3147
+ commands: ["audit", "audit:verify", "audit:export", "health"]
3148
+ },
3149
+ {
3150
+ name: "Hooks",
3151
+ symbol: SYMBOLS.link,
3152
+ commands: [
3153
+ "hook:install",
3154
+ "hook:uninstall",
3155
+ "hook:run",
3156
+ "hook"
3157
+ ]
3158
+ },
3159
+ {
3160
+ name: "Agent Memory",
3161
+ symbol: SYMBOLS.ghost,
3162
+ commands: ["remember", "recall", "forget", "analyze", "agent"]
3163
+ },
3164
+ {
3165
+ name: "Security & Governance",
3166
+ symbol: SYMBOLS.lock,
3167
+ commands: ["approve", "approvals", "policy"]
3168
+ }
3169
+ ];
3170
+ var GroupedHelp = class extends Help {
3171
+ formatHelp(cmd, helper) {
3172
+ if (cmd.parent) return super.formatHelp(cmd, helper);
3173
+ const termWidth = helper.padWidth(cmd, helper);
3174
+ const helpWidth = helper.helpWidth ?? 80;
3175
+ const sections = [];
3176
+ sections.push(
3177
+ `${c.bold("qring")} ${c.dim(`v${PACKAGE_VERSION}`)} ${c.dim("\u2014")} ${c.dim(
3178
+ "quantum keyring for AI coding tools"
3179
+ )}`
3180
+ );
3181
+ sections.push("");
3182
+ sections.push(`${c.bold("Usage:")} qring [options] [command]`);
3183
+ const options = helper.visibleOptions(cmd);
3184
+ if (options.length > 0) {
3185
+ sections.push("");
3186
+ sections.push(c.bold("Options:"));
3187
+ for (const opt of options) {
3188
+ const term = helper.optionTerm(opt);
3189
+ const desc = helper.optionDescription(opt);
3190
+ sections.push(formatRow(term, desc, termWidth, helpWidth));
3191
+ }
3192
+ }
3193
+ const commands = helper.visibleCommands(cmd);
3194
+ if (commands.length === 0) return sections.join("\n") + "\n";
3195
+ const byName = /* @__PURE__ */ new Map();
3196
+ for (const sub of commands) byName.set(sub.name(), sub);
3197
+ const used = /* @__PURE__ */ new Set();
3198
+ for (const group of COMMAND_GROUPS) {
3199
+ const members = group.commands.map((n) => byName.get(n)).filter((x) => Boolean(x));
3200
+ if (members.length === 0) continue;
3201
+ for (const m of members) used.add(m.name());
3202
+ sections.push("");
3203
+ sections.push(`${group.symbol} ${c.bold(group.name)}`);
3204
+ for (const sub of members) {
3205
+ sections.push(
3206
+ formatRow(
3207
+ helper.subcommandTerm(sub),
3208
+ helper.subcommandDescription(sub),
3209
+ termWidth,
3210
+ helpWidth
3211
+ )
3212
+ );
3213
+ }
3214
+ }
3215
+ const other = commands.filter((sub) => !used.has(sub.name()));
3216
+ if (other.length > 0) {
3217
+ sections.push("");
3218
+ sections.push(c.bold("Other:"));
3219
+ for (const sub of other) {
3220
+ sections.push(
3221
+ formatRow(
3222
+ helper.subcommandTerm(sub),
3223
+ helper.subcommandDescription(sub),
3224
+ termWidth,
3225
+ helpWidth
3226
+ )
3227
+ );
3228
+ }
3229
+ }
3230
+ sections.push("");
3231
+ sections.push(
3232
+ c.dim(
3233
+ `Run ${c.bold("qring <command> --help")} to see detailed flags for any command.`
3234
+ )
3235
+ );
3236
+ return sections.join("\n") + "\n";
3237
+ }
3238
+ };
3239
+ function formatRow(term, description, termWidth, helpWidth) {
3240
+ const indent = " ";
3241
+ const gap = " ";
3242
+ const padded = term.padEnd(termWidth);
3243
+ if (!description) return `${indent}${padded}`;
3244
+ const available = Math.max(helpWidth - indent.length - termWidth - gap.length, 20);
3245
+ const desc = wrapText(description, available).replace(
3246
+ /\n/g,
3247
+ `
3248
+ ${indent}${" ".repeat(termWidth)}${gap}`
3249
+ );
3250
+ return `${indent}${padded}${gap}${c.dim(desc)}`;
3251
+ }
3252
+ function wrapText(text, width) {
3253
+ if (text.length <= width) return text;
3254
+ const lines = [];
3255
+ let current = "";
3256
+ for (const word of text.split(/\s+/)) {
3257
+ if (current.length + word.length + 1 > width) {
3258
+ lines.push(current);
3259
+ current = word;
3260
+ } else {
3261
+ current = current ? `${current} ${word}` : word;
3262
+ }
3263
+ }
3264
+ if (current) lines.push(current);
3265
+ return lines.join("\n");
3266
+ }
3267
+ function createProgram() {
3268
+ const program2 = new Command().name("qring").description(
3269
+ `${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
3270
+ ).version(PACKAGE_VERSION).option("--json", "Machine-readable JSON output for supported commands").configureHelp({
3271
+ sortSubcommands: false,
3272
+ sortOptions: false
3273
+ }).showHelpAfterError(c.dim("(run `qring --help` for usage)"));
3274
+ program2.createHelp = () => new GroupedHelp();
3275
+ registerSecretsCommands(program2);
3276
+ registerProjectCommands(program2);
3277
+ registerQuantumCommands(program2);
3278
+ registerValidationCommands(program2);
3279
+ registerToolingCommands(program2);
3280
+ registerAuditCommands(program2);
3281
+ registerHookCommands(program2);
3282
+ registerAgentCommands(program2);
3283
+ registerSecurityCommands(program2);
2822
3284
  return program2;
2823
3285
  }
2824
3286