@hasna/configs 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2506,6 +2506,185 @@ async function applyConfigs(configs, opts = {}) {
2506
2506
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
2507
2507
  import { extname, join as join2 } from "path";
2508
2508
  import { homedir as homedir2 } from "os";
2509
+
2510
+ // src/lib/redact.ts
2511
+ var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
2512
+ var VALUE_PATTERNS = [
2513
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
2514
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
2515
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
2516
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
2517
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
2518
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
2519
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
2520
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
2521
+ ];
2522
+ var MIN_SECRET_VALUE_LEN = 8;
2523
+ function redactShell(content) {
2524
+ const redacted = [];
2525
+ const lines = content.split(`
2526
+ `);
2527
+ const out = [];
2528
+ for (let i = 0;i < lines.length; i++) {
2529
+ const line = lines[i];
2530
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2531
+ if (m) {
2532
+ const [, prefix, key, eq, quote, value] = m;
2533
+ if (shouldRedactKeyValue(key, value)) {
2534
+ const reason = reasonFor(key, value);
2535
+ redacted.push({ varName: key, line: i + 1, reason });
2536
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
2537
+ continue;
2538
+ }
2539
+ }
2540
+ out.push(line);
2541
+ }
2542
+ return { content: out.join(`
2543
+ `), redacted, isTemplate: redacted.length > 0 };
2544
+ }
2545
+ function redactJson(content) {
2546
+ const redacted = [];
2547
+ const lines = content.split(`
2548
+ `);
2549
+ const out = [];
2550
+ for (let i = 0;i < lines.length; i++) {
2551
+ const line = lines[i];
2552
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
2553
+ if (m) {
2554
+ const [, prefix, key, value, comma, trail] = m;
2555
+ if (shouldRedactKeyValue(key, value)) {
2556
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2557
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2558
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
2559
+ continue;
2560
+ }
2561
+ }
2562
+ let newLine = line;
2563
+ for (const { re, reason } of VALUE_PATTERNS) {
2564
+ newLine = newLine.replace(re, (match) => {
2565
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
2566
+ redacted.push({ varName, line: i + 1, reason });
2567
+ return `{{${varName}}}`;
2568
+ });
2569
+ }
2570
+ out.push(newLine);
2571
+ }
2572
+ return { content: out.join(`
2573
+ `), redacted, isTemplate: redacted.length > 0 };
2574
+ }
2575
+ function redactToml(content) {
2576
+ const redacted = [];
2577
+ const lines = content.split(`
2578
+ `);
2579
+ const out = [];
2580
+ for (let i = 0;i < lines.length; i++) {
2581
+ const line = lines[i];
2582
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2583
+ if (m) {
2584
+ const [, indent, key, eq, quote, value] = m;
2585
+ if (shouldRedactKeyValue(key, value)) {
2586
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2587
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2588
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
2589
+ continue;
2590
+ }
2591
+ }
2592
+ out.push(line);
2593
+ }
2594
+ return { content: out.join(`
2595
+ `), redacted, isTemplate: redacted.length > 0 };
2596
+ }
2597
+ function redactIni(content) {
2598
+ const redacted = [];
2599
+ const lines = content.split(`
2600
+ `);
2601
+ const out = [];
2602
+ for (let i = 0;i < lines.length; i++) {
2603
+ const line = lines[i];
2604
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
2605
+ if (authM) {
2606
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
2607
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
2608
+ continue;
2609
+ }
2610
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
2611
+ if (m) {
2612
+ const [, indent, key, eq, value] = m;
2613
+ if (shouldRedactKeyValue(key, value)) {
2614
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2615
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2616
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
2617
+ continue;
2618
+ }
2619
+ }
2620
+ out.push(line);
2621
+ }
2622
+ return { content: out.join(`
2623
+ `), redacted, isTemplate: redacted.length > 0 };
2624
+ }
2625
+ function redactGeneric(content) {
2626
+ const redacted = [];
2627
+ const lines = content.split(`
2628
+ `);
2629
+ const out = [];
2630
+ for (let i = 0;i < lines.length; i++) {
2631
+ let line = lines[i];
2632
+ for (const { re, reason } of VALUE_PATTERNS) {
2633
+ line = line.replace(re, (match) => {
2634
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2635
+ redacted.push({ varName, line: i + 1, reason });
2636
+ return `{{${varName}}}`;
2637
+ });
2638
+ }
2639
+ out.push(line);
2640
+ }
2641
+ return { content: out.join(`
2642
+ `), redacted, isTemplate: redacted.length > 0 };
2643
+ }
2644
+ function shouldRedactKeyValue(key, value) {
2645
+ if (!value || value.startsWith("{{"))
2646
+ return false;
2647
+ if (value.length < MIN_SECRET_VALUE_LEN)
2648
+ return false;
2649
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
2650
+ return false;
2651
+ if (SECRET_KEY_PATTERN.test(key))
2652
+ return true;
2653
+ for (const { re } of VALUE_PATTERNS) {
2654
+ if (re.test(value))
2655
+ return true;
2656
+ }
2657
+ return false;
2658
+ }
2659
+ function reasonFor(key, value) {
2660
+ if (SECRET_KEY_PATTERN.test(key))
2661
+ return `secret key name: ${key}`;
2662
+ for (const { re, reason } of VALUE_PATTERNS) {
2663
+ if (re.test(value))
2664
+ return reason;
2665
+ }
2666
+ return "secret value pattern";
2667
+ }
2668
+ function redactContent(content, format) {
2669
+ switch (format) {
2670
+ case "shell":
2671
+ return redactShell(content);
2672
+ case "json":
2673
+ return redactJson(content);
2674
+ case "toml":
2675
+ return redactToml(content);
2676
+ case "ini":
2677
+ return redactIni(content);
2678
+ default:
2679
+ return redactGeneric(content);
2680
+ }
2681
+ }
2682
+ function scanSecrets(content, format) {
2683
+ const r = redactContent(content, format);
2684
+ return r.redacted;
2685
+ }
2686
+
2687
+ // src/lib/sync.ts
2509
2688
  var KNOWN_CONFIGS = [
2510
2689
  { path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
2511
2690
  { path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
@@ -2547,17 +2726,18 @@ async function syncKnown(opts = {}) {
2547
2726
  for (const f of mdFiles) {
2548
2727
  const abs2 = join2(absDir, f);
2549
2728
  const targetPath = abs2.replace(home, "~");
2550
- const content = readFileSync2(abs2, "utf-8");
2729
+ const raw = readFileSync2(abs2, "utf-8");
2730
+ const { content, isTemplate } = redactContent(raw, "markdown");
2551
2731
  const name = `claude-rules-${f}`;
2552
2732
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2553
2733
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
2554
2734
  if (!existing) {
2555
2735
  if (!opts.dryRun)
2556
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath }, d);
2736
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
2557
2737
  result.added++;
2558
2738
  } else if (existing.content !== content) {
2559
2739
  if (!opts.dryRun)
2560
- updateConfig(existing.id, { content }, d);
2740
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2561
2741
  result.updated++;
2562
2742
  } else {
2563
2743
  result.unchanged++;
@@ -2571,11 +2751,13 @@ async function syncKnown(opts = {}) {
2571
2751
  continue;
2572
2752
  }
2573
2753
  try {
2574
- const content = readFileSync2(abs, "utf-8");
2575
- if (content.length > 500000) {
2754
+ const rawContent = readFileSync2(abs, "utf-8");
2755
+ if (rawContent.length > 500000) {
2576
2756
  result.skipped.push(known.path + " (too large)");
2577
2757
  continue;
2578
2758
  }
2759
+ const fmt = known.format ?? detectFormat(abs);
2760
+ const { content, isTemplate } = redactContent(rawContent, fmt);
2579
2761
  const targetPath = abs.replace(home, "~");
2580
2762
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
2581
2763
  if (!existing) {
@@ -2584,17 +2766,18 @@ async function syncKnown(opts = {}) {
2584
2766
  name: known.name,
2585
2767
  category: known.category,
2586
2768
  agent: known.agent,
2587
- format: known.format ?? detectFormat(abs),
2769
+ format: fmt,
2588
2770
  content,
2589
2771
  target_path: known.kind === "reference" ? null : targetPath,
2590
2772
  kind: known.kind ?? "file",
2591
- description: known.description
2773
+ description: known.description,
2774
+ is_template: isTemplate
2592
2775
  }, d);
2593
2776
  }
2594
2777
  result.added++;
2595
2778
  } else if (existing.content !== content) {
2596
2779
  if (!opts.dryRun)
2597
- updateConfig(existing.id, { content }, d);
2780
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2598
2781
  result.updated++;
2599
2782
  } else {
2600
2783
  result.unchanged++;
@@ -2890,7 +3073,9 @@ program.command("add <path>").description("Ingest a file into the config DB").op
2890
3073
  console.error(chalk.red(`File not found: ${abs}`));
2891
3074
  process.exit(1);
2892
3075
  }
2893
- const content = readFileSync4(abs, "utf-8");
3076
+ const rawContent = readFileSync4(abs, "utf-8");
3077
+ const fmt = detectFormat(abs);
3078
+ const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
2894
3079
  const targetPath = abs.startsWith(homedir3()) ? abs.replace(homedir3(), "~") : abs;
2895
3080
  const name = opts.name || filePath.split("/").pop();
2896
3081
  const config = createConfig({
@@ -2899,11 +3084,17 @@ program.command("add <path>").description("Ingest a file into the config DB").op
2899
3084
  category: opts.category ?? detectCategory(abs),
2900
3085
  agent: opts.agent ?? detectAgent(abs),
2901
3086
  target_path: opts.kind === "reference" ? null : targetPath,
2902
- format: detectFormat(abs),
3087
+ format: fmt,
2903
3088
  content,
2904
- is_template: opts.template ?? false
3089
+ is_template: (opts.template ?? false) || isTemplate
2905
3090
  });
2906
3091
  console.log(chalk.green("\u2713") + ` Added: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
3092
+ if (redacted.length > 0) {
3093
+ console.log(chalk.yellow(` \u26A0 Redacted ${redacted.length} secret(s):`));
3094
+ for (const r of redacted)
3095
+ console.log(chalk.yellow(` line ${r.line}: {{${r.varName}}} \u2014 ${r.reason}`));
3096
+ console.log(chalk.dim(" Config stored as a template. Use `configs template vars` to see placeholders."));
3097
+ }
2907
3098
  });
2908
3099
  program.command("apply <id>").description("Apply a config to its target_path on disk").option("--dry-run", "preview without writing").option("--force", "overwrite even if unchanged").action(async (id, opts) => {
2909
3100
  try {
@@ -3129,5 +3320,29 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
3129
3320
  process.exit(1);
3130
3321
  }
3131
3322
  });
3323
+ program.command("scan [id]").description("Scan configs for secrets. Omit id to scan all.").option("--fix", "redact found secrets in-place").action(async (id, opts) => {
3324
+ const configs = id ? [getConfig(id)] : listConfigs({ kind: "file" });
3325
+ let total = 0;
3326
+ for (const c of configs) {
3327
+ const secrets = scanSecrets(c.content, c.format);
3328
+ if (secrets.length === 0)
3329
+ continue;
3330
+ total += secrets.length;
3331
+ console.log(chalk.yellow(`\u26A0 ${c.slug}`) + chalk.dim(` \u2014 ${secrets.length} secret(s):`));
3332
+ for (const s of secrets)
3333
+ console.log(` line ${s.line}: ${chalk.red(s.varName)} \u2014 ${s.reason}`);
3334
+ if (opts.fix) {
3335
+ const { content, isTemplate } = redactContent(c.content, c.format);
3336
+ updateConfig(c.id, { content, is_template: isTemplate });
3337
+ console.log(chalk.green(" \u2713 Redacted and updated."));
3338
+ }
3339
+ }
3340
+ if (total === 0) {
3341
+ console.log(chalk.green("\u2713") + " No secrets detected.");
3342
+ } else if (!opts.fix) {
3343
+ console.log(chalk.yellow(`
3344
+ Run with --fix to redact in-place.`));
3345
+ }
3346
+ });
3132
3347
  program.version(pkg.version).name("configs");
3133
3348
  program.parse(process.argv);
package/dist/index.js CHANGED
@@ -528,6 +528,179 @@ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as
528
528
  import { extname, join as join3 } from "path";
529
529
  import { homedir as homedir3 } from "os";
530
530
 
531
+ // src/lib/redact.ts
532
+ var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
533
+ var VALUE_PATTERNS = [
534
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
535
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
536
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
537
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
538
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
539
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
540
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
541
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
542
+ ];
543
+ var MIN_SECRET_VALUE_LEN = 8;
544
+ function redactShell(content) {
545
+ const redacted = [];
546
+ const lines = content.split(`
547
+ `);
548
+ const out = [];
549
+ for (let i = 0;i < lines.length; i++) {
550
+ const line = lines[i];
551
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
552
+ if (m) {
553
+ const [, prefix, key, eq, quote, value] = m;
554
+ if (shouldRedactKeyValue(key, value)) {
555
+ const reason = reasonFor(key, value);
556
+ redacted.push({ varName: key, line: i + 1, reason });
557
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
558
+ continue;
559
+ }
560
+ }
561
+ out.push(line);
562
+ }
563
+ return { content: out.join(`
564
+ `), redacted, isTemplate: redacted.length > 0 };
565
+ }
566
+ function redactJson(content) {
567
+ const redacted = [];
568
+ const lines = content.split(`
569
+ `);
570
+ const out = [];
571
+ for (let i = 0;i < lines.length; i++) {
572
+ const line = lines[i];
573
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
574
+ if (m) {
575
+ const [, prefix, key, value, comma, trail] = m;
576
+ if (shouldRedactKeyValue(key, value)) {
577
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
578
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
579
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
580
+ continue;
581
+ }
582
+ }
583
+ let newLine = line;
584
+ for (const { re, reason } of VALUE_PATTERNS) {
585
+ newLine = newLine.replace(re, (match) => {
586
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
587
+ redacted.push({ varName, line: i + 1, reason });
588
+ return `{{${varName}}}`;
589
+ });
590
+ }
591
+ out.push(newLine);
592
+ }
593
+ return { content: out.join(`
594
+ `), redacted, isTemplate: redacted.length > 0 };
595
+ }
596
+ function redactToml(content) {
597
+ const redacted = [];
598
+ const lines = content.split(`
599
+ `);
600
+ const out = [];
601
+ for (let i = 0;i < lines.length; i++) {
602
+ const line = lines[i];
603
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
604
+ if (m) {
605
+ const [, indent, key, eq, quote, value] = m;
606
+ if (shouldRedactKeyValue(key, value)) {
607
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
608
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
609
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
610
+ continue;
611
+ }
612
+ }
613
+ out.push(line);
614
+ }
615
+ return { content: out.join(`
616
+ `), redacted, isTemplate: redacted.length > 0 };
617
+ }
618
+ function redactIni(content) {
619
+ const redacted = [];
620
+ const lines = content.split(`
621
+ `);
622
+ const out = [];
623
+ for (let i = 0;i < lines.length; i++) {
624
+ const line = lines[i];
625
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
626
+ if (authM) {
627
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
628
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
629
+ continue;
630
+ }
631
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
632
+ if (m) {
633
+ const [, indent, key, eq, value] = m;
634
+ if (shouldRedactKeyValue(key, value)) {
635
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
636
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
637
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
638
+ continue;
639
+ }
640
+ }
641
+ out.push(line);
642
+ }
643
+ return { content: out.join(`
644
+ `), redacted, isTemplate: redacted.length > 0 };
645
+ }
646
+ function redactGeneric(content) {
647
+ const redacted = [];
648
+ const lines = content.split(`
649
+ `);
650
+ const out = [];
651
+ for (let i = 0;i < lines.length; i++) {
652
+ let line = lines[i];
653
+ for (const { re, reason } of VALUE_PATTERNS) {
654
+ line = line.replace(re, (match) => {
655
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
656
+ redacted.push({ varName, line: i + 1, reason });
657
+ return `{{${varName}}}`;
658
+ });
659
+ }
660
+ out.push(line);
661
+ }
662
+ return { content: out.join(`
663
+ `), redacted, isTemplate: redacted.length > 0 };
664
+ }
665
+ function shouldRedactKeyValue(key, value) {
666
+ if (!value || value.startsWith("{{"))
667
+ return false;
668
+ if (value.length < MIN_SECRET_VALUE_LEN)
669
+ return false;
670
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
671
+ return false;
672
+ if (SECRET_KEY_PATTERN.test(key))
673
+ return true;
674
+ for (const { re } of VALUE_PATTERNS) {
675
+ if (re.test(value))
676
+ return true;
677
+ }
678
+ return false;
679
+ }
680
+ function reasonFor(key, value) {
681
+ if (SECRET_KEY_PATTERN.test(key))
682
+ return `secret key name: ${key}`;
683
+ for (const { re, reason } of VALUE_PATTERNS) {
684
+ if (re.test(value))
685
+ return reason;
686
+ }
687
+ return "secret value pattern";
688
+ }
689
+ function redactContent(content, format) {
690
+ switch (format) {
691
+ case "shell":
692
+ return redactShell(content);
693
+ case "json":
694
+ return redactJson(content);
695
+ case "toml":
696
+ return redactToml(content);
697
+ case "ini":
698
+ return redactIni(content);
699
+ default:
700
+ return redactGeneric(content);
701
+ }
702
+ }
703
+
531
704
  // src/lib/sync-dir.ts
532
705
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
533
706
  import { join as join2, relative } from "path";
@@ -649,17 +822,18 @@ async function syncKnown(opts = {}) {
649
822
  for (const f of mdFiles) {
650
823
  const abs2 = join3(absDir, f);
651
824
  const targetPath = abs2.replace(home, "~");
652
- const content = readFileSync3(abs2, "utf-8");
825
+ const raw = readFileSync3(abs2, "utf-8");
826
+ const { content, isTemplate } = redactContent(raw, "markdown");
653
827
  const name = `claude-rules-${f}`;
654
828
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
655
829
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
656
830
  if (!existing) {
657
831
  if (!opts.dryRun)
658
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath }, d);
832
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
659
833
  result.added++;
660
834
  } else if (existing.content !== content) {
661
835
  if (!opts.dryRun)
662
- updateConfig(existing.id, { content }, d);
836
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
663
837
  result.updated++;
664
838
  } else {
665
839
  result.unchanged++;
@@ -673,11 +847,13 @@ async function syncKnown(opts = {}) {
673
847
  continue;
674
848
  }
675
849
  try {
676
- const content = readFileSync3(abs, "utf-8");
677
- if (content.length > 500000) {
850
+ const rawContent = readFileSync3(abs, "utf-8");
851
+ if (rawContent.length > 500000) {
678
852
  result.skipped.push(known.path + " (too large)");
679
853
  continue;
680
854
  }
855
+ const fmt = known.format ?? detectFormat(abs);
856
+ const { content, isTemplate } = redactContent(rawContent, fmt);
681
857
  const targetPath = abs.replace(home, "~");
682
858
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
683
859
  if (!existing) {
@@ -686,17 +862,18 @@ async function syncKnown(opts = {}) {
686
862
  name: known.name,
687
863
  category: known.category,
688
864
  agent: known.agent,
689
- format: known.format ?? detectFormat(abs),
865
+ format: fmt,
690
866
  content,
691
867
  target_path: known.kind === "reference" ? null : targetPath,
692
868
  kind: known.kind ?? "file",
693
- description: known.description
869
+ description: known.description,
870
+ is_template: isTemplate
694
871
  }, d);
695
872
  }
696
873
  result.added++;
697
874
  } else if (existing.content !== content) {
698
875
  if (!opts.dryRun)
699
- updateConfig(existing.id, { content }, d);
876
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
700
877
  result.updated++;
701
878
  } else {
702
879
  result.unchanged++;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Secret redaction engine.
3
+ *
4
+ * Detects and replaces sensitive values with {{VARNAME}} template placeholders
5
+ * so they are NEVER stored in the DB. The config becomes a template that can
6
+ * be rendered with real values at apply-time.
7
+ *
8
+ * Strategy:
9
+ * 1. Key-name matching — if an assignment's LHS looks like a secret key name
10
+ * 2. Value-pattern matching — known token formats (npm, GitHub, Anthropic, etc.)
11
+ * regardless of key name
12
+ */
13
+ export interface RedactResult {
14
+ content: string;
15
+ redacted: RedactedVar[];
16
+ isTemplate: boolean;
17
+ }
18
+ export interface RedactedVar {
19
+ varName: string;
20
+ line: number;
21
+ reason: string;
22
+ }
23
+ export type RedactFormat = "shell" | "json" | "toml" | "ini" | "markdown" | "text" | "yaml";
24
+ export declare function redactContent(content: string, format: RedactFormat): RedactResult;
25
+ /** Detect secrets without modifying content. Returns list of findings. */
26
+ export declare function scanSecrets(content: string, format: RedactFormat): RedactedVar[];
27
+ /** Returns true if content contains any detectable secrets. */
28
+ export declare function hasSecrets(content: string, format: RedactFormat): boolean;
29
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/lib/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAsMD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5F,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,YAAY,CAQjF;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,WAAW,EAAE,CAGhF;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAEzE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=redact.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.test.d.ts","sourceRoot":"","sources":["../../src/lib/redact.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/lib/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQhD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,EAiCtC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAuEhF;AAGD,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBlF;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAqBjD;AAGD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAU/D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CASzD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAQ3D;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/lib/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAShD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,EAiCtC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4EhF;AAGD,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBlF;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAqBjD;AAGD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAU/D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CASzD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAQ3D;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/configs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "AI coding agent configuration manager — store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",