skillcellar 0.3.0 → 0.6.1

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.
@@ -0,0 +1,75 @@
1
+ // What stops a backup: a line that looks like a key, a token or a password. Each pattern is bounded,
2
+ // so a line of any length is read in one pass, and a placeholder such as ${NAME}, $NAME, <your key>
3
+ // or your_api_key is not a secret. A finding is a reason to look, not proof: the backup stops either
4
+ // way, and a key of a shape no pattern knows is not found.
5
+
6
+ // A key stands alone: nothing alphanumeric right before or after it, so STRIPE_API_KEY= and "apiKey"
7
+ // count, which a word boundary would miss between _ and a letter.
8
+ const EDGE = String.raw`(?<![A-Za-z0-9])`;
9
+ const END = String.raw`(?![A-Za-z0-9])`;
10
+ // A value that is not a placeholder, a variable, a template, an example or a reference into config or code.
11
+ const NOT_PLACEHOLDER = String.raw`(?!\$\{|\$[A-Za-z_]|<|\{\{|%|your[_-]|xxx|example|placeholder|change[_-]?me|redacted|none\b|null\b|true\b|false\b|\*{3}|process\.|os\.|env\.|config\.|settings\.|this\.|self\.)`;
12
+ // The characters an assigned or bearer value is read with.
13
+ const VALUE = String.raw`[A-Za-z0-9_\-./+=]`;
14
+ // A word in either case, for a pattern that minds case.
15
+ const anyCase = (word) => word.replace(/[a-z]/g, (letter) => `[${letter}${letter.toUpperCase()}]`);
16
+ const EXAMPLE_WORDS = ["dummy", "fake", "sample", "example", "placeholder", "redacted", "change[_-]?me"].map(anyCase).join("|");
17
+ // A value as documentation writes one, not a key: elided (abc123... or abc123…), or holding a word,
18
+ // not part of a longer one, that calls it an example. Real keys are neither.
19
+ const NOT_EXAMPLE = String.raw`(?!${VALUE}{0,255}?(?:\.\.\.|(?<![A-Za-z])(?:${EXAMPLE_WORDS})(?![A-Za-z])))(?!${VALUE}{1,255}…)`;
20
+ // Code such as base64.b64encode( is no value; a quoted value is one, whatever it holds.
21
+ const NOT_CALL = String.raw`(?:(?<=["'\x60])|(?![A-Za-z_]\w{0,60}(?:\.[A-Za-z_]\w{0,60}){1,5}\())`;
22
+
23
+ const PATTERNS = [
24
+ ["a private key", /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/],
25
+ ["an AWS access key", new RegExp(String.raw`${EDGE}AKIA[0-9A-Z]{16}${END}`)],
26
+ ["a GitHub token", new RegExp(String.raw`${EDGE}(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255})${END}`)],
27
+ ["an OpenAI or Anthropic key", new RegExp(String.raw`${EDGE}sk-(?:proj-|ant-|svcacct-)?[A-Za-z0-9_-]{20,255}(?![A-Za-z0-9_-])`)],
28
+ ["a Slack token", new RegExp(String.raw`${EDGE}xox[abeprs]-[A-Za-z0-9-]{10,255}(?![A-Za-z0-9-])`)],
29
+ ["a Stripe key", new RegExp(String.raw`${EDGE}(?:[sr]k_(?:live|test)_[A-Za-z0-9]{16,255}|whsec_[A-Za-z0-9]{16,255})${END}`)],
30
+ ["a Google API key", new RegExp(String.raw`${EDGE}AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9_-])`)],
31
+ ["a SkillCellar API key", new RegExp(String.raw`${EDGE}skc_[A-Za-z0-9]{64}${END}`)],
32
+ ["an npm token", new RegExp(String.raw`${EDGE}npm_[A-Za-z0-9]{36}${END}`)],
33
+ ["a Neon API key", new RegExp(String.raw`${EDGE}napi_[a-z0-9]{40,255}${END}`)],
34
+ ["a Resend, Hugging Face or Groq key", new RegExp(String.raw`${EDGE}(?:re_[A-Za-z0-9_]{20,255}|hf_[A-Za-z0-9]{20,255}|gsk_[A-Za-z0-9]{20,255})${END}`)],
35
+ ["a JSON Web Token", new RegExp(String.raw`${EDGE}eyJ[A-Za-z0-9_-]{10,2000}\.[A-Za-z0-9_-]{10,2000}\.[A-Za-z0-9_-]{10,2000}(?![A-Za-z0-9_-])`)],
36
+ // user:password@host in an address of any scheme, unless the password is a placeholder.
37
+ ["a password in an address", /[a-z][a-z0-9+.-]{0,30}:\/\/[^\s/@:'"]{1,100}:(?!\$\{)[^\s/@'"]{1,200}@/i],
38
+ ["a bearer or basic credential", new RegExp(String.raw`\b(?:Bearer|Basic)\s{1,5}(?!\$\{|\$[A-Za-z_]|<)${NOT_EXAMPLE}[A-Za-z0-9_\-./+=]{16,2000}(?![A-Za-z0-9_\-./+=])`)],
39
+ // A key named as one, assigned a value with a digit in it, as random keys have; the name may be quoted, as in JSON.
40
+ [
41
+ "a key or password in an assignment",
42
+ new RegExp(
43
+ String.raw`${EDGE}(?:api[_-]?key|apikey|secret[_-]?(?:access[_-]?)?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|password|passwd|secret|token)${END}["'\x60]?\s{0,5}[:=]\s{0,5}["'\x60]?${NOT_PLACEHOLDER}${NOT_EXAMPLE}${NOT_CALL}(?=[A-Za-z0-9_\-./+=]{0,254}[0-9])[A-Za-z0-9_\-./+=]{8,255}`,
44
+ "i",
45
+ ),
46
+ ],
47
+ ];
48
+
49
+ /**
50
+ * The first line of a text that looks like a secret, as `{ line, kind }` (the line counted from
51
+ * 1), or null. Lines are read one by one, so the file's size sets the cost, never a pattern.
52
+ */
53
+ export function findSecret(text) {
54
+ const lines = text.split("\n");
55
+ for (let index = 0; index < lines.length; index += 1) {
56
+ const line = lines[index];
57
+ for (const [kind, pattern] of PATTERNS) if (pattern.test(line)) return { line: index + 1, kind };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /** The placeholders a sanitized MCP config holds, such as ${CONTEXT7_KEY}, in order and once each. */
63
+ export function placeholdersIn(text) {
64
+ return [...new Set(Array.from(text.matchAll(/\$\{([A-Z0-9_]{1,80})\}/g), (match) => match[0]))];
65
+ }
66
+
67
+ /**
68
+ * References in an MCP config that Claude Code would expand from the environment and that are not
69
+ * plain placeholders: `${VAR:-default}`, `$VAR`, or a placeholder of another shape. Each is a reason
70
+ * to read the server's config before adding it, since it reaches into the user's environment.
71
+ */
72
+ export function oddReferencesIn(text) {
73
+ const all = Array.from(text.matchAll(/\$\{[^}]{0,200}\}|\$[A-Za-z_][A-Za-z0-9_]{0,80}/g), (match) => match[0]);
74
+ return [...new Set(all.filter((reference) => !/^\$\{[A-Z0-9_]{1,80}\}$/.test(reference)))];
75
+ }