cortad 0.1.12 → 0.1.14
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/lib/keys.mjs +38 -0
- package/local.mjs +17 -2
- package/package.json +2 -1
package/lib/keys.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A file that carries a key stays on this machine, whatever git thinks of it: a catalogue that keeps
|
|
2
|
+
// an AES key per video is tracked like code and is no more shareable than a .env.
|
|
3
|
+
// The header alone is code that looks for keys; a key has its body after it.
|
|
4
|
+
const PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----(?:\\n|\s)*[A-Za-z0-9+/=]{40,}/;
|
|
5
|
+
const PROVIDER_TOKEN = /\b((?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|xox[abprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{35}|[rs]k_live_[0-9A-Za-z]{24,}|glpat-[0-9A-Za-z_-]{20,}))\b/g;
|
|
6
|
+
// A long literal under a key-like name. A name read from the environment is not a literal, so
|
|
7
|
+
// `apiKey: process.env.KEY` never matches.
|
|
8
|
+
const NAMED_LITERAL = /["']?[\w-]*(?:key|secret|token|passw(?:or)?d|aes|iv|salt)["']?\s*[:=]\s*["']([A-Fa-f0-9]{32,}|[A-Za-z0-9+/_-]{32,}={0,2})["']/gi;
|
|
9
|
+
|
|
10
|
+
// A real key is random: digits and letters, many distinct characters. "sk-live-should-never-leak"
|
|
11
|
+
// in a test is a name for a key, not one.
|
|
12
|
+
const random = (s) => /\d/.test(s) && /[A-Za-z]/.test(s) && new Set(s).size >= 10;
|
|
13
|
+
const any = (re, text) => [...text.matchAll(re)].some((m) => random(m[1]));
|
|
14
|
+
|
|
15
|
+
export function holdsKeys(text, values = []) {
|
|
16
|
+
return PRIVATE_KEY.test(text) || any(PROVIDER_TOKEN, text) || any(NAMED_LITERAL, text) || values.some((v) => text.includes(v));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The values worth looking for elsewhere: a variable whose name says it is a secret, from a real env
|
|
20
|
+
// file. An example file's values are placeholders, and a bucket or model name is not a key.
|
|
21
|
+
const SECRET_NAME = /KEY|SECRET|TOKEN|PASSW|PRIVATE|CREDENTIAL/i;
|
|
22
|
+
const EXAMPLE = /\.(example|sample|template|dist)$/i;
|
|
23
|
+
export function secretEnvValues(envFiles, read) {
|
|
24
|
+
const values = [];
|
|
25
|
+
for (const file of envFiles) {
|
|
26
|
+
if (EXAMPLE.test(file)) continue;
|
|
27
|
+
let text = "";
|
|
28
|
+
try { text = read(file); } catch { continue; }
|
|
29
|
+
for (const line of text.split("\n")) {
|
|
30
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
31
|
+
if (!m || !SECRET_NAME.test(m[1])) continue;
|
|
32
|
+
const v = m[2].trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
33
|
+
// A value of few distinct characters is a placeholder or a published test key, not a secret.
|
|
34
|
+
if (v.length >= 12 && !/\s/.test(v) && new Set(v).size >= 8) values.push(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return values;
|
|
38
|
+
}
|
package/local.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// (lib/lock.mjs) and cannot write your code at all. Nothing here touches git. Your environment
|
|
12
12
|
// never leaves this machine. Ctrl-C ends everything.
|
|
13
13
|
|
|
14
|
+
import { holdsKeys, secretEnvValues } from "./lib/keys.mjs";
|
|
14
15
|
import { spawn, execFile, execFileSync } from "node:child_process";
|
|
15
16
|
import { createHash } from "node:crypto";
|
|
16
17
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
|
|
@@ -119,6 +120,11 @@ function shareable(rel) {
|
|
|
119
120
|
try { execFileSync("git", ["-C", root, "check-ignore", "-q", rel], { stdio: "ignore" }); return false; } catch (e) { return e.status === 1; }
|
|
120
121
|
}
|
|
121
122
|
let listed = null;
|
|
123
|
+
// Read the same way for the upload and for any read the engine asks for later.
|
|
124
|
+
let envKeys = [];
|
|
125
|
+
function carriesKey(rel) {
|
|
126
|
+
try { return holdsKeys(readFileSync(join(root, rel), "utf8"), envKeys); } catch { return false; }
|
|
127
|
+
}
|
|
122
128
|
|
|
123
129
|
// Values from your env files, read here and only here, so nothing a command prints can carry one.
|
|
124
130
|
function secretValues(envFiles) {
|
|
@@ -280,7 +286,8 @@ const readable = (p) => {
|
|
|
280
286
|
let landed; try { landed = realpathSync(r); } catch { return null; }
|
|
281
287
|
const home = realpathSync(root);
|
|
282
288
|
if (!landed.startsWith(home + sep) || ENV_FILE.test(basename(landed)) || SECRET_PATH.test(landed)) return null;
|
|
283
|
-
|
|
289
|
+
const rel = relative(home, landed);
|
|
290
|
+
return shareable(rel) && !carriesKey(rel) ? landed : null;
|
|
284
291
|
};
|
|
285
292
|
// A shell line runs with a plain environment: the app's own process reads its .env itself, and
|
|
286
293
|
// nothing a world sends inherits this terminal's keys.
|
|
@@ -746,9 +753,15 @@ const envFiles = [];
|
|
|
746
753
|
const files = [];
|
|
747
754
|
listed = gitListed();
|
|
748
755
|
walk(root, 0, files, envFiles, 0);
|
|
756
|
+
secrets = secretValues(envFiles);
|
|
757
|
+
envKeys = secretEnvValues(envFiles, (f) => readFileSync(f, "utf8"));
|
|
758
|
+
const kept = [];
|
|
759
|
+
for (let i = files.length - 1; i >= 0; i -= 1) if (carriesKey(files[i])) kept.unshift(...files.splice(i, 1));
|
|
749
760
|
// --explain: what this would send and start, from this folder, and nothing else. No network, no
|
|
750
761
|
// app started, nothing written. For the person (or the agent) who reads before running.
|
|
751
762
|
if (explain) {
|
|
763
|
+
const going = new Set([...files, ...kept]);
|
|
764
|
+
const left = listed ? [...listed].filter((f) => !going.has(f) && !ENV_FILE.test(basename(f))).sort() : [];
|
|
752
765
|
const bytes = files.reduce((n, f) => { try { return n + statSync(join(root, f)).size; } catch { return n; } }, 0);
|
|
753
766
|
const plan = startPlan({ root, typed: flag("--start"), onPath });
|
|
754
767
|
const rel = (f) => relative(root, f) || ".";
|
|
@@ -757,6 +770,8 @@ if (explain) {
|
|
|
757
770
|
``,
|
|
758
771
|
`talks to ${origin.origin}, localhost (your app's port only), and your own model providers, to ask each which models your key can use`,
|
|
759
772
|
`would send ${files.length} source files, ${Math.round(bytes / 1024)} KB, once${listed ? " (what git would commit)" : ""}`,
|
|
773
|
+
...(kept.length ? [`kept here ${kept.length} file${kept.length === 1 ? " that holds" : "s that hold"} keys: ${kept.slice(0, 6).join(", ")}${kept.length > 6 ? ", ..." : ""}`] : []),
|
|
774
|
+
...(left.length ? [`also not sent ${left.length} tracked data, media or lock file${left.length === 1 ? "" : "s"}: ${left.slice(0, 6).join(", ")}${left.length > 6 ? ", ..." : ""}`] : []),
|
|
760
775
|
`not sent anything git ignores, env files (${envFiles.length} here: ${envFiles.slice(0, 6).map(rel).join(", ") || "none"}), key files, data files, node_modules, .git`,
|
|
761
776
|
`env files values read here only: to hide them in replies, to sign in a test account, and to ask your providers what your keys reach. Variable names and whether a switch is on or off go up; no value does`,
|
|
762
777
|
`would start ${flag("--port") ? `nothing: uses your app on port ${flag("--port")}` : plan?.cmd ? `${plan.cmd} (in ${rel(plan.cwd)})` : plan?.noServer ? "nothing: this repository has no server to run" : "asks you how your app starts"}`,
|
|
@@ -769,7 +784,7 @@ if (explain) {
|
|
|
769
784
|
].join("\n"));
|
|
770
785
|
process.exit(0);
|
|
771
786
|
}
|
|
772
|
-
|
|
787
|
+
if (kept.length) say(`kept on this machine, ${kept.length === 1 ? "it holds" : "they hold"} keys: ${kept.join(", ")}`);
|
|
773
788
|
const keepSecret = (v) => { if (v && v.length >= 12 && !secrets.includes(v)) secrets.push(v); };
|
|
774
789
|
identities = makeIdentities({ root, work, envFiles, sourceFiles: () => files, say, keepSecret, appDir: () => appDir });
|
|
775
790
|
// One message sent in their own app tells us the door for certain. The route and the body go up,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortad",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Connects the AI app on your machine to Cortad for test conversations. No dependencies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cortad": "local.mjs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"lib/door.mjs",
|
|
12
12
|
"lib/identities.mjs",
|
|
13
13
|
"lib/listing.mjs",
|
|
14
|
+
"lib/keys.mjs",
|
|
14
15
|
"lib/lock.mjs",
|
|
15
16
|
"lib/mint.mjs",
|
|
16
17
|
"lib/pyhook/sitecustomize.py",
|