@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 +226 -11
- package/dist/index.js +185 -8
- package/dist/lib/redact.d.ts +29 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.test.d.ts +2 -0
- package/dist/lib/redact.test.d.ts.map +1 -0
- package/dist/lib/sync.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
2575
|
-
if (
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
677
|
-
if (
|
|
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:
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"redact.test.d.ts","sourceRoot":"","sources":["../../src/lib/redact.test.ts"],"names":[],"mappings":""}
|
package/dist/lib/sync.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|