@i4ctime/q-ring 0.3.1 → 0.4.0

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