@i4ctime/q-ring 0.3.2 → 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) {
@@ -419,8 +697,8 @@ function buildOpts(cmd) {
419
697
  function createProgram() {
420
698
  const program2 = new Command().name("qring").description(
421
699
  `${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
422
- ).version("0.2.0");
423
- program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").action(async (key, value, cmd) => {
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) => {
424
702
  const opts = buildOpts(cmd);
425
703
  if (!value) {
426
704
  value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
@@ -434,7 +712,9 @@ function createProgram() {
434
712
  ttlSeconds: cmd.ttl,
435
713
  expiresAt: cmd.expires,
436
714
  description: cmd.description,
437
- 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
438
718
  };
439
719
  if (cmd.env) {
440
720
  const existing = getEnvelope(key, opts);
@@ -478,9 +758,29 @@ function createProgram() {
478
758
  process.exit(1);
479
759
  }
480
760
  });
481
- program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").action((cmd) => {
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) => {
482
762
  const opts = buildOpts(cmd);
483
- 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
+ }
484
784
  if (entries.length === 0) {
485
785
  console.log(c.dim("No secrets found"));
486
786
  return;
@@ -593,11 +893,200 @@ function createProgram() {
593
893
  }
594
894
  console.log();
595
895
  });
596
- program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").action((cmd) => {
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) => {
597
897
  const opts = buildOpts(cmd);
598
- 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
+ });
599
904
  process.stdout.write(output + "\n");
600
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
+ });
601
1090
  program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
602
1091
  const result = collapseEnvironment({
603
1092
  projectPath: cmd.projectPath ?? process.cwd()
@@ -657,6 +1146,24 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
657
1146
  );
658
1147
  }
659
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
+ );
660
1167
  const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
661
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) => {
662
1169
  const id = tunnelCreate(value, {
@@ -884,8 +1391,166 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
884
1391
  }
885
1392
  console.log();
886
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
+ });
887
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) => {
888
- const { startDashboardServer } = await import("./dashboard-X3ONQFLV.js");
1553
+ const { startDashboardServer } = await import("./dashboard-HVIQO6NT.js");
889
1554
  const { exec } = await import("child_process");
890
1555
  const { platform } = await import("os");
891
1556
  const port = Number(cmd.port);