codiedev 0.10.0-beta.14 → 0.10.0-beta.16
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.js +5 -33
- package/dist/commands/scrub.js +8 -0
- package/dist/hook.js +4 -5
- package/dist/knownSecrets.js +214 -0
- package/dist/localScrub.js +62 -12
- package/dist/localScrubReport.js +6 -3
- package/dist/scrub/extraRules.js +12 -0
- package/dist/scrub/rules.js +149 -15
- package/dist/scrub/scrubProviderRules.js +145 -0
- package/dist/utils.js +0 -6
- package/package.json +4 -4
- package/dist/commands/createTicket.js +0 -182
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,6 @@ const ask_1 = require("./commands/ask");
|
|
|
44
44
|
const ping_1 = require("./commands/ping");
|
|
45
45
|
const inbox_1 = require("./commands/inbox");
|
|
46
46
|
const note_1 = require("./commands/note");
|
|
47
|
-
const createTicket_1 = require("./commands/createTicket");
|
|
48
47
|
const doctor_1 = require("./commands/doctor");
|
|
49
48
|
const search_1 = require("./commands/search");
|
|
50
49
|
const library_1 = require("./commands/library");
|
|
@@ -90,14 +89,6 @@ Artifacts:
|
|
|
90
89
|
team's collective memory. Returns an
|
|
91
90
|
LLM-synthesized answer with citations
|
|
92
91
|
pointing at sessions + artifacts.
|
|
93
|
-
codiedev create-ticket [<prompt>] Forward-write a scoped ticket plan.
|
|
94
|
-
Claude investigates the codebase, asks
|
|
95
|
-
"single repo or multiple? where?", then
|
|
96
|
-
emits a plan with N+1 split. Lands in
|
|
97
|
-
/portal/research/<id>; push to Jira from there.
|
|
98
|
-
codiedev create-ticket --submit <path|->
|
|
99
|
-
Internal: Claude calls this itself with
|
|
100
|
-
a plan JSON to submit. Use - for stdin.
|
|
101
92
|
|
|
102
93
|
Messaging:
|
|
103
94
|
codiedev ping <user> "<msg>" [--with <key>]
|
|
@@ -337,25 +328,6 @@ codiedev note — capture a passing thought, linked to your session
|
|
|
337
328
|
unignore: IGNORE_HELP,
|
|
338
329
|
ignored: IGNORE_HELP,
|
|
339
330
|
capture: IGNORE_HELP,
|
|
340
|
-
"create-ticket": `
|
|
341
|
-
codiedev create-ticket — forward-write a scoped ticket plan
|
|
342
|
-
|
|
343
|
-
Default path:
|
|
344
|
-
codiedev create-ticket "I want to add SSO with Google"
|
|
345
|
-
|
|
346
|
-
Bare mode (no --submit) prints skill instructions for Claude to follow.
|
|
347
|
-
Claude asks where your repos live, investigates them locally, and emits a
|
|
348
|
-
plan as JSON. The CLI then re-invokes itself with --submit to upload it.
|
|
349
|
-
|
|
350
|
-
The plan lands at /portal/research/<id> as a ready-state research session.
|
|
351
|
-
Single ticket renders as one ticket; multi-ticket renders as a folder/group.
|
|
352
|
-
Push to Jira from the portal.
|
|
353
|
-
|
|
354
|
-
Examples:
|
|
355
|
-
codiedev create-ticket
|
|
356
|
-
codiedev create-ticket "I want to add Google SSO"
|
|
357
|
-
codiedev create-ticket --submit /tmp/plan.json # internal, called by Claude
|
|
358
|
-
`.trim(),
|
|
359
331
|
};
|
|
360
332
|
const DOCS = `
|
|
361
333
|
Codiedev — usage guide
|
|
@@ -466,8 +438,10 @@ Run 'codiedev help <command>' for each, or see the quick list:
|
|
|
466
438
|
`.trim();
|
|
467
439
|
const REMOVED_COMMANDS = {
|
|
468
440
|
promote: "Share an auto-saved spec with 'Promote to team' in the portal's My artifacts list.",
|
|
469
|
-
"reverse-ticket": "
|
|
470
|
-
reverseTicket: "
|
|
441
|
+
"reverse-ticket": "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
|
|
442
|
+
reverseTicket: "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
|
|
443
|
+
"create-ticket": "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
|
|
444
|
+
createTicket: "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
|
|
471
445
|
react: "React to feed posts in the portal.",
|
|
472
446
|
};
|
|
473
447
|
function removedCommand(command) {
|
|
@@ -591,11 +565,9 @@ async function main() {
|
|
|
591
565
|
case "reverse-ticket":
|
|
592
566
|
case "reverseTicket":
|
|
593
567
|
case "react":
|
|
594
|
-
removedCommand(command);
|
|
595
|
-
return;
|
|
596
568
|
case "create-ticket":
|
|
597
569
|
case "createTicket":
|
|
598
|
-
|
|
570
|
+
removedCommand(command);
|
|
599
571
|
return;
|
|
600
572
|
case "search":
|
|
601
573
|
await (0, search_1.runSearch)(rest);
|
package/dist/commands/scrub.js
CHANGED
|
@@ -21,6 +21,14 @@ themselves never leave this machine.
|
|
|
21
21
|
--path <file> Only this file (repeatable)
|
|
22
22
|
--session <id> With --path <opencode.db>: only this OpenCode/Codie session
|
|
23
23
|
--min-age <min> Skip files changed in the last <min> minutes (default 60)
|
|
24
|
+
|
|
25
|
+
The hourly background run can also remove your own secrets (values from
|
|
26
|
+
your credential files and .env files) if your admin allows it. It reads
|
|
27
|
+
~/.aws/credentials, ~/.npmrc, ~/.pypirc, ~/.netrc, ~/.git-credentials,
|
|
28
|
+
~/.config/gh/hosts.yml, ~/.docker/config.json, ~/.config/hub,
|
|
29
|
+
~/.gem/credentials, ~/.config/openai/credentials, and .env / .env.* in the
|
|
30
|
+
repos your sessions start in. Values stay on this machine; only if your admin
|
|
31
|
+
chose to check uploaded sessions too is a keyed fingerprint of each sent.
|
|
24
32
|
`;
|
|
25
33
|
function parseScrubArgs(args) {
|
|
26
34
|
let dryRun = false;
|
package/dist/hook.js
CHANGED
|
@@ -445,9 +445,8 @@ async function main() {
|
|
|
445
445
|
}
|
|
446
446
|
if (process.argv[2] === "local-scrub") {
|
|
447
447
|
try {
|
|
448
|
-
const {
|
|
449
|
-
|
|
450
|
-
await runAndReport();
|
|
448
|
+
const { runScheduledScrub } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
|
|
449
|
+
await runScheduledScrub();
|
|
451
450
|
}
|
|
452
451
|
catch {
|
|
453
452
|
}
|
|
@@ -501,7 +500,7 @@ async function main() {
|
|
|
501
500
|
if (source === "codex")
|
|
502
501
|
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
503
502
|
if (source === "codex")
|
|
504
|
-
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
|
|
503
|
+
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
|
|
505
504
|
if (source !== "copilot")
|
|
506
505
|
await runDiscover(hookInput.prompt);
|
|
507
506
|
return;
|
|
@@ -543,7 +542,7 @@ async function main() {
|
|
|
543
542
|
process.argv[2] === "session-start-opencode" ||
|
|
544
543
|
process.argv[2] === "session-start-pi") {
|
|
545
544
|
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
546
|
-
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
|
|
545
|
+
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
|
|
547
546
|
const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
|
|
548
547
|
if (packNag && process.argv[2] === "session-start")
|
|
549
548
|
process.stdout.write(`${packNag}\n`);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hintOf = exports.CREDENTIAL_FILES = void 0;
|
|
37
|
+
exports.worthMatching = worthMatching;
|
|
38
|
+
exports.parseAssignments = parseAssignments;
|
|
39
|
+
exports.credentialFiles = credentialFiles;
|
|
40
|
+
exports.collectKnownSecrets = collectKnownSecrets;
|
|
41
|
+
exports.knownSecretRule = knownSecretRule;
|
|
42
|
+
exports.knownHash = knownHash;
|
|
43
|
+
exports.fingerprints = fingerprints;
|
|
44
|
+
exports.uploadKnownSecrets = uploadKnownSecrets;
|
|
45
|
+
exports.rememberCwd = rememberCwd;
|
|
46
|
+
exports.recentCwds = recentCwds;
|
|
47
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const rules_1 = require("./scrub/rules");
|
|
52
|
+
const MAX_SECRETS = 500;
|
|
53
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
54
|
+
const MAX_CWDS = 30;
|
|
55
|
+
function worthMatching(v) {
|
|
56
|
+
if (v.length < 16 || v.length > 512)
|
|
57
|
+
return false;
|
|
58
|
+
if (/\s/.test(v) || /^[a-z][a-z0-9+.-]*:\/\//i.test(v))
|
|
59
|
+
return false;
|
|
60
|
+
if (/^(?:true|false|null|undefined|localhost)$/i.test(v) || /^[\d.]+$/.test(v))
|
|
61
|
+
return false;
|
|
62
|
+
if (/^[~./]/.test(v) || /\$\{|\$\(/.test(v))
|
|
63
|
+
return false;
|
|
64
|
+
if ((0, rules_1.looksLikeTestValue)(v))
|
|
65
|
+
return false;
|
|
66
|
+
return (0, rules_1.shannonEntropy)(v) >= 3.5;
|
|
67
|
+
}
|
|
68
|
+
const unquote = (v) => v.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
69
|
+
const short = (p) => p.split(path.sep).filter(Boolean).slice(-2).join("/");
|
|
70
|
+
function parseAssignments(text) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const line of text.split(/\r?\n/)) {
|
|
73
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_./:@-][\w./:@-]*)\s*[=:]\s*(.+?)\s*$/);
|
|
74
|
+
if (!m || line.trim().startsWith("#") || line.trim().startsWith(";"))
|
|
75
|
+
continue;
|
|
76
|
+
out.push({ name: m[1].replace(/^\/\/[^:]*:/, ""), value: unquote(m[2].replace(/\s+#.*$/, "")) });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function read(file) {
|
|
81
|
+
try {
|
|
82
|
+
const st = fs.statSync(file);
|
|
83
|
+
if (!st.isFile() || st.size > MAX_FILE_BYTES)
|
|
84
|
+
return null;
|
|
85
|
+
return fs.readFileSync(file, "utf8");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.CREDENTIAL_FILES = [
|
|
92
|
+
".aws/credentials", ".npmrc", ".pypirc", ".netrc", ".git-credentials", ".config/gh/hosts.yml",
|
|
93
|
+
".docker/config.json", ".config/hub", ".gem/credentials", ".config/openai/credentials",
|
|
94
|
+
];
|
|
95
|
+
function credentialFiles(home, cwds) {
|
|
96
|
+
const global = exports.CREDENTIAL_FILES.map((p) => path.join(home, p));
|
|
97
|
+
const envs = [];
|
|
98
|
+
for (const cwd of cwds) {
|
|
99
|
+
let entries = [];
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(cwd);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (/^\.env(?:\.[\w-]+)?$/.test(e) && !/\.(?:example|sample|template|dist|defaults?)$/i.test(e))
|
|
108
|
+
envs.push(path.join(cwd, e));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return [...global, ...envs];
|
|
112
|
+
}
|
|
113
|
+
function collectKnownSecrets(home = os.homedir(), cwds = []) {
|
|
114
|
+
const seen = new Map();
|
|
115
|
+
const add = (value, file, name) => {
|
|
116
|
+
if (seen.size >= MAX_SECRETS || !worthMatching(value) || seen.has(value))
|
|
117
|
+
return;
|
|
118
|
+
seen.set(value, { value, file: short(file), ...(name ? { name: name.slice(0, 64) } : {}) });
|
|
119
|
+
};
|
|
120
|
+
for (const file of credentialFiles(home, cwds)) {
|
|
121
|
+
const text = read(file);
|
|
122
|
+
if (!text)
|
|
123
|
+
continue;
|
|
124
|
+
const base = path.basename(file);
|
|
125
|
+
if (base === "config.json") {
|
|
126
|
+
try {
|
|
127
|
+
const auths = JSON.parse(text).auths ?? {};
|
|
128
|
+
for (const [host, a] of Object.entries(auths)) {
|
|
129
|
+
if (a.auth)
|
|
130
|
+
add(a.auth, file, `docker ${host}`);
|
|
131
|
+
if (a.identitytoken)
|
|
132
|
+
add(a.identitytoken, file, `docker ${host}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (base === ".git-credentials") {
|
|
139
|
+
for (const m of text.matchAll(/^https?:\/\/[^:\s]+:([^@\s]+)@(\S+)$/gm))
|
|
140
|
+
add(decodeURIComponent(m[1]), file, `git ${m[2]}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (base === ".netrc") {
|
|
144
|
+
for (const m of text.matchAll(/\bpassword\s+(\S+)/g))
|
|
145
|
+
add(m[1], file, "netrc password");
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for (const { name, value } of parseAssignments(text))
|
|
149
|
+
add(value, file, name);
|
|
150
|
+
}
|
|
151
|
+
return [...seen.values()];
|
|
152
|
+
}
|
|
153
|
+
function knownSecretRule(secrets) {
|
|
154
|
+
if (secrets.length === 0)
|
|
155
|
+
return null;
|
|
156
|
+
const forms = new Set();
|
|
157
|
+
for (const s of secrets) {
|
|
158
|
+
forms.add(s.value);
|
|
159
|
+
const escaped = JSON.stringify(s.value).slice(1, -1);
|
|
160
|
+
if (escaped !== s.value)
|
|
161
|
+
forms.add(escaped);
|
|
162
|
+
}
|
|
163
|
+
const alt = [...forms].sort((a, b) => b.length - a.length).map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
164
|
+
return { name: "known-secret", re: new RegExp(`(?<![A-Za-z0-9])(?:${alt})(?![A-Za-z0-9])`, "g") };
|
|
165
|
+
}
|
|
166
|
+
function knownHash(keyHex, value) {
|
|
167
|
+
return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(value).digest("hex").slice(0, 32);
|
|
168
|
+
}
|
|
169
|
+
const hintOf = (value) => value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
|
|
170
|
+
exports.hintOf = hintOf;
|
|
171
|
+
function fingerprints(keyHex, secrets) {
|
|
172
|
+
return secrets.map((s) => ({ hash: knownHash(keyHex, s.value), hint: (0, exports.hintOf)(s.value), ...(s.name ? { name: s.name } : {}), file: s.file }));
|
|
173
|
+
}
|
|
174
|
+
async function uploadKnownSecrets(config, keyHex, secrets, fetchImpl = fetch) {
|
|
175
|
+
if (!config?.backendUrl || !config.token || !/^[0-9a-f]{64}$/.test(keyHex))
|
|
176
|
+
return false;
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetchImpl(`${config.backendUrl}/api/cli/knownSecrets`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.token}` },
|
|
181
|
+
body: JSON.stringify({ secrets: fingerprints(keyHex, secrets) }),
|
|
182
|
+
signal: AbortSignal.timeout(15_000),
|
|
183
|
+
});
|
|
184
|
+
return res.ok;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function rememberCwd(stateDir, cwd) {
|
|
191
|
+
if (!cwd || !path.isAbsolute(cwd))
|
|
192
|
+
return;
|
|
193
|
+
try {
|
|
194
|
+
const file = path.join(stateDir, "cwds.json");
|
|
195
|
+
let list = [];
|
|
196
|
+
try {
|
|
197
|
+
list = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
list = [cwd, ...list.filter((c) => c !== cwd)].slice(0, MAX_CWDS);
|
|
201
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
202
|
+
fs.writeFileSync(file, JSON.stringify(list), { mode: 0o600 });
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
}
|
|
206
|
+
function recentCwds(stateDir) {
|
|
207
|
+
try {
|
|
208
|
+
const list = JSON.parse(fs.readFileSync(path.join(stateDir, "cwds.json"), "utf8"));
|
|
209
|
+
return Array.isArray(list) ? list.filter((c) => typeof c === "string") : [];
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/localScrub.js
CHANGED
|
@@ -47,13 +47,17 @@ exports.cleanOpenCodeDb = cleanOpenCodeDb;
|
|
|
47
47
|
exports.countCursorDb = countCursorDb;
|
|
48
48
|
exports.runLocalScrub = runLocalScrub;
|
|
49
49
|
exports.runAndReport = runAndReport;
|
|
50
|
+
exports.scrubPolicy = scrubPolicy;
|
|
50
51
|
exports.scrubAllowed = scrubAllowed;
|
|
52
|
+
exports.formatRules = formatRules;
|
|
53
|
+
exports.runScheduledScrub = runScheduledScrub;
|
|
51
54
|
exports.maybeSpawnLocalScrub = maybeSpawnLocalScrub;
|
|
52
55
|
const fs = __importStar(require("fs"));
|
|
53
56
|
const os = __importStar(require("os"));
|
|
54
57
|
const path = __importStar(require("path"));
|
|
55
58
|
const child_process_1 = require("child_process");
|
|
56
|
-
const
|
|
59
|
+
const extraRules_1 = require("./scrub/extraRules");
|
|
60
|
+
const knownSecrets_1 = require("./knownSecrets");
|
|
57
61
|
const harnessDirs_1 = require("./harnessDirs");
|
|
58
62
|
const copilotHooks_1 = require("./copilotHooks");
|
|
59
63
|
const configDir_1 = require("./configDir");
|
|
@@ -93,6 +97,12 @@ function sessionStores(env = process.env, home = os.homedir(), stateDir = (0, co
|
|
|
93
97
|
root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
|
|
94
98
|
match: (f) => path.basename(f) === "workspace.yaml",
|
|
95
99
|
},
|
|
100
|
+
{
|
|
101
|
+
harness: "copilot",
|
|
102
|
+
kind: "json",
|
|
103
|
+
root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
|
|
104
|
+
match: (f) => path.basename(f) === "index.json" && path.basename(path.dirname(f)) === "rewind-file-snapshots",
|
|
105
|
+
},
|
|
96
106
|
{ harness: "pi", kind: "jsonl", root: path.join((0, harnessDirs_1.piAgentDir)(env, home), "sessions"), match: isJsonl },
|
|
97
107
|
{ harness: "pi", kind: "jsonl", root: path.join(stateDir, "managed", "pi", "agent", "sessions"), match: isJsonl },
|
|
98
108
|
{ harness: "opencode", kind: "json", root: path.join(stateDir, "opencode-captures"), match: (f) => f.endsWith(".json") },
|
|
@@ -117,7 +127,7 @@ function cursorIdeDb(env = process.env, home = os.homedir(), platform = process.
|
|
|
117
127
|
}
|
|
118
128
|
function cleanValue(value, count) {
|
|
119
129
|
if (typeof value === "string") {
|
|
120
|
-
const r = (0,
|
|
130
|
+
const r = (0, extraRules_1.scrub)(value);
|
|
121
131
|
count.n += r.redactions.length;
|
|
122
132
|
return r.text;
|
|
123
133
|
}
|
|
@@ -141,7 +151,7 @@ function parses(text) {
|
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
function cleanJson(text) {
|
|
144
|
-
const raw = (0,
|
|
154
|
+
const raw = (0, extraRules_1.scrub)(text);
|
|
145
155
|
if (raw.redactions.length === 0)
|
|
146
156
|
return { text, redactions: 0 };
|
|
147
157
|
if (parses(raw.text) || !parses(text))
|
|
@@ -168,13 +178,13 @@ function cleanJsonl(text) {
|
|
|
168
178
|
return redactions === 0 ? { text, redactions: 0 } : { text: lines.join("\n"), redactions };
|
|
169
179
|
}
|
|
170
180
|
function cleanText(text) {
|
|
171
|
-
const r = (0,
|
|
181
|
+
const r = (0, extraRules_1.scrub)(text);
|
|
172
182
|
return { text: r.text, redactions: r.redactions.length };
|
|
173
183
|
}
|
|
174
184
|
function cleanYaml(text) {
|
|
175
185
|
let redactions = 0;
|
|
176
186
|
const lines = text.split("\n").map((line) => {
|
|
177
|
-
const r = (0,
|
|
187
|
+
const r = (0, extraRules_1.scrub)(line);
|
|
178
188
|
if (r.redactions.length === 0)
|
|
179
189
|
return line;
|
|
180
190
|
redactions += r.redactions.length;
|
|
@@ -333,7 +343,7 @@ function cleanOpenCodeDb(file, deps) {
|
|
|
333
343
|
for (const [id, d] of sessions) {
|
|
334
344
|
const row = get.all(id)[0];
|
|
335
345
|
if (typeof row?.title === "string")
|
|
336
|
-
d.title = (0,
|
|
346
|
+
d.title = (0, extraRules_1.scrub)(row.title).text.slice(0, 80);
|
|
337
347
|
if (typeof row?.directory === "string")
|
|
338
348
|
d.cwd = row.directory;
|
|
339
349
|
}
|
|
@@ -416,7 +426,7 @@ function countCursorDb(file, kind) {
|
|
|
416
426
|
const details = [...chats].map(([chat, keys]) => ({
|
|
417
427
|
harness: "cursor",
|
|
418
428
|
sessionKey: chat,
|
|
419
|
-
...(titles.get(chat) ? { title: (0,
|
|
429
|
+
...(titles.get(chat) ? { title: (0, extraRules_1.scrub)(titles.get(chat)).text.slice(0, 80) } : {}),
|
|
420
430
|
keys,
|
|
421
431
|
status: "not_removed",
|
|
422
432
|
reason: "cursor_chat_store",
|
|
@@ -657,26 +667,65 @@ async function runAndReport(opts = {}) {
|
|
|
657
667
|
}
|
|
658
668
|
return { ...summary, reported };
|
|
659
669
|
}
|
|
660
|
-
|
|
670
|
+
const defaultConfig = () => {
|
|
661
671
|
const { readConfig } = require("./utils");
|
|
662
672
|
return readConfig();
|
|
663
|
-
}
|
|
673
|
+
};
|
|
674
|
+
const OFF = { enabled: false, formats: [], ownSecrets: "off" };
|
|
675
|
+
async function scrubPolicy(fetchImpl = fetch, getConfig = defaultConfig) {
|
|
664
676
|
try {
|
|
665
677
|
const config = getConfig();
|
|
666
678
|
if (!config?.token || !config.backendUrl)
|
|
667
|
-
return
|
|
679
|
+
return OFF;
|
|
668
680
|
const res = await fetchImpl(`${config.backendUrl}/api/cli/localScrubPolicy`, {
|
|
669
681
|
headers: { Authorization: `Bearer ${config.token}` },
|
|
670
682
|
signal: AbortSignal.timeout(10_000),
|
|
671
683
|
});
|
|
672
684
|
if (!res.ok)
|
|
673
|
-
return
|
|
685
|
+
return OFF;
|
|
674
686
|
const body = (await res.json());
|
|
675
|
-
|
|
687
|
+
const formats = Array.isArray(body.formats)
|
|
688
|
+
? body.formats.filter((f) => f && typeof f.name === "string" && typeof f.source === "string" && f.source.length <= 300).slice(0, 50)
|
|
689
|
+
: [];
|
|
690
|
+
const ownSecrets = body.ownSecrets === "off" || body.ownSecrets === "server" ? body.ownSecrets : "laptop";
|
|
691
|
+
const key = typeof body.fingerprintKey === "string" && /^[0-9a-f]{64}$/.test(body.fingerprintKey) ? body.fingerprintKey : undefined;
|
|
692
|
+
return { enabled: body.enabled === true, formats, ownSecrets, ...(key ? { fingerprintKey: key } : {}) };
|
|
676
693
|
}
|
|
677
694
|
catch {
|
|
695
|
+
return OFF;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async function scrubAllowed(fetchImpl = fetch, getConfig = defaultConfig) {
|
|
699
|
+
return (await scrubPolicy(fetchImpl, getConfig)).enabled;
|
|
700
|
+
}
|
|
701
|
+
function formatRules(formats) {
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const f of formats) {
|
|
704
|
+
try {
|
|
705
|
+
out.push({ name: `custom:${f.name}`, re: new RegExp(`(?<![A-Za-z0-9])${f.source}(?![A-Za-z0-9_-])`, "g"), ...(f.keyword ? { keywords: [f.keyword] } : {}) });
|
|
706
|
+
}
|
|
707
|
+
catch { }
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
async function runScheduledScrub(opts = {}) {
|
|
712
|
+
const getConfig = opts.getConfig ?? defaultConfig;
|
|
713
|
+
const policy = await scrubPolicy(opts.fetchImpl ?? fetch, getConfig);
|
|
714
|
+
if (!policy.enabled)
|
|
678
715
|
return false;
|
|
716
|
+
const stateDir = opts.stateDir ?? (0, configDir_1.codiedevStateDir)();
|
|
717
|
+
const known = policy.ownSecrets === "off" ? [] : (0, knownSecrets_1.collectKnownSecrets)(opts.home ?? os.homedir(), (0, knownSecrets_1.recentCwds)(scrubDir(stateDir)));
|
|
718
|
+
if (policy.ownSecrets === "server" && policy.fingerprintKey)
|
|
719
|
+
await (0, knownSecrets_1.uploadKnownSecrets)(getConfig(), policy.fingerprintKey, known, opts.fetchImpl ?? fetch);
|
|
720
|
+
const knownRule = (0, knownSecrets_1.knownSecretRule)(known);
|
|
721
|
+
(0, extraRules_1.setExtraRules)([...formatRules(policy.formats), ...(knownRule ? [knownRule] : [])]);
|
|
722
|
+
try {
|
|
723
|
+
await runAndReport({ stateDir });
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
(0, extraRules_1.setExtraRules)([]);
|
|
679
727
|
}
|
|
728
|
+
return true;
|
|
680
729
|
}
|
|
681
730
|
function maybeSpawnLocalScrub(opts = {}) {
|
|
682
731
|
try {
|
|
@@ -685,6 +734,7 @@ function maybeSpawnLocalScrub(opts = {}) {
|
|
|
685
734
|
return "disabled";
|
|
686
735
|
const now = opts.now ?? Date.now();
|
|
687
736
|
const dir = scrubDir(opts.stateDir ?? (0, configDir_1.codiedevStateDir)());
|
|
737
|
+
(0, knownSecrets_1.rememberCwd)(dir, opts.cwd);
|
|
688
738
|
const stamp = path.join(dir, "last-run");
|
|
689
739
|
try {
|
|
690
740
|
const last = fs.statSync(stamp).mtimeMs;
|
package/dist/localScrubReport.js
CHANGED
|
@@ -46,11 +46,12 @@ const fs = __importStar(require("fs"));
|
|
|
46
46
|
const os = __importStar(require("os"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const rules_1 = require("./scrub/rules");
|
|
49
|
+
const extraRules_1 = require("./scrub/extraRules");
|
|
49
50
|
const captureExclusions_1 = require("./captureExclusions");
|
|
50
51
|
const MAX_KEYS_PER_SESSION = 20;
|
|
51
52
|
const TITLE_LEN = 80;
|
|
52
53
|
function keysIn(text) {
|
|
53
|
-
const { redactions } = (0,
|
|
54
|
+
const { redactions } = (0, extraRules_1.scrub)(text);
|
|
54
55
|
if (redactions.length === 0)
|
|
55
56
|
return [];
|
|
56
57
|
const hints = new Map();
|
|
@@ -181,8 +182,10 @@ function sessionKeyOf(harness, file) {
|
|
|
181
182
|
const base = path.basename(file);
|
|
182
183
|
if (base === "history.jsonl")
|
|
183
184
|
return "history";
|
|
184
|
-
if (harness === "copilot")
|
|
185
|
-
|
|
185
|
+
if (harness === "copilot") {
|
|
186
|
+
const dir = path.dirname(file);
|
|
187
|
+
return path.basename(path.basename(dir) === "rewind-file-snapshots" ? path.dirname(dir) : dir);
|
|
188
|
+
}
|
|
186
189
|
const uuid = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
187
190
|
if (uuid)
|
|
188
191
|
return uuid[1];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setExtraRules = setExtraRules;
|
|
4
|
+
exports.scrub = scrub;
|
|
5
|
+
const rules_1 = require("./rules");
|
|
6
|
+
let extra = [];
|
|
7
|
+
function setExtraRules(rules) {
|
|
8
|
+
extra = rules;
|
|
9
|
+
}
|
|
10
|
+
function scrub(text) {
|
|
11
|
+
return extra.length ? (0, rules_1.scrubSecrets)(text, { extraRules: extra }) : (0, rules_1.scrubSecrets)(text);
|
|
12
|
+
}
|
package/dist/scrub/rules.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GENERIC_RULES = void 0;
|
|
4
|
+
exports.shannonEntropy = shannonEntropy;
|
|
3
5
|
exports.scrubSecrets = scrubSecrets;
|
|
6
|
+
exports.redactSpans = redactSpans;
|
|
7
|
+
exports.looksLikeTestValue = looksLikeTestValue;
|
|
4
8
|
exports.secretFindings = secretFindings;
|
|
5
9
|
exports.possibleSecrets = possibleSecrets;
|
|
6
10
|
exports.redactPossible = redactPossible;
|
|
11
|
+
const scrubProviderRules_1 = require("./scrubProviderRules");
|
|
12
|
+
function shannonEntropy(s) {
|
|
13
|
+
if (!s)
|
|
14
|
+
return 0;
|
|
15
|
+
const counts = new Map();
|
|
16
|
+
for (const ch of s)
|
|
17
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
18
|
+
let h = 0;
|
|
19
|
+
for (const n of counts.values()) {
|
|
20
|
+
const p = n / s.length;
|
|
21
|
+
h -= p * Math.log2(p);
|
|
22
|
+
}
|
|
23
|
+
return h;
|
|
24
|
+
}
|
|
7
25
|
const SECRET_REF_KEY = /_(?:NAME|ID|ARN|FILE|PATH|URL|URI|HOST|REGION|BUCKET|PREFIX|ENABLED|TYPE|VERSION|COUNT)$/i;
|
|
8
26
|
function looksSecret(v) {
|
|
9
27
|
if (v.includes("<") || v.includes(">"))
|
|
@@ -34,6 +52,7 @@ function looksSecret(v) {
|
|
|
34
52
|
}
|
|
35
53
|
const PLACEHOLDER = /x{6,}|\.\.\.|your|example|placeholder|changeme|redacted|dummy|fake|<|\$\{|\*{4,}/i;
|
|
36
54
|
const notPlaceholder = (v) => !PLACEHOLDER.test(v);
|
|
55
|
+
const notTestValue = (v) => notPlaceholder(v) && !looksLikeTestValue(v);
|
|
37
56
|
const URL_PASSWORD_PLACEHOLDER = /^(?:pass(?:word)?|secret|pwd|test|admin|root|guest)$/i;
|
|
38
57
|
const RULES = [
|
|
39
58
|
{
|
|
@@ -82,19 +101,61 @@ const RULES = [
|
|
|
82
101
|
m.input[m.index - 1] !== ":" &&
|
|
83
102
|
looksSecret(v),
|
|
84
103
|
},
|
|
104
|
+
{ name: "supabase-token", re: /\bsbp_[a-f0-9]{40}\b/g, validate: notTestValue },
|
|
105
|
+
{ name: "groq-key", re: /\bgsk_[A-Za-z0-9]{48,56}\b/g, validate: notTestValue },
|
|
106
|
+
{ name: "xai-key", re: /\bxai-[A-Za-z0-9]{70,90}\b/g, validate: notTestValue },
|
|
107
|
+
{ name: "replicate-token", re: /\br8_[A-Za-z0-9]{37}\b/g, validate: notTestValue },
|
|
108
|
+
...scrubProviderRules_1.PROVIDER_RULES.map((r) => ({
|
|
109
|
+
name: r.name,
|
|
110
|
+
re: r.re,
|
|
111
|
+
...(r.group !== undefined ? { group: r.group } : {}),
|
|
112
|
+
...(r.firstGroup ? { firstGroup: true } : {}),
|
|
113
|
+
keywords: r.keywords,
|
|
114
|
+
fallback: true,
|
|
115
|
+
validate: (v) => notTestValue(v) && (r.entropy === undefined || shannonEntropy(v) >= r.entropy),
|
|
116
|
+
})),
|
|
85
117
|
];
|
|
86
|
-
|
|
118
|
+
const ALL_KEYWORDS = [...new Set(RULES.flatMap((r) => r.keywords ?? []))].sort((a, b) => b.length - a.length);
|
|
119
|
+
const KEYWORD_RE = new RegExp(ALL_KEYWORDS.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "gi");
|
|
120
|
+
const KEYWORD_PARTS = new Map(ALL_KEYWORDS.map((k) => [k, ALL_KEYWORDS.filter((p) => k.includes(p))]));
|
|
121
|
+
function keywordsIn(input) {
|
|
122
|
+
const found = new Set();
|
|
123
|
+
if (!ALL_KEYWORDS.length)
|
|
124
|
+
return found;
|
|
125
|
+
KEYWORD_RE.lastIndex = 0;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = KEYWORD_RE.exec(input)) !== null) {
|
|
128
|
+
for (const k of KEYWORD_PARTS.get(m[0].toLowerCase()) ?? [])
|
|
129
|
+
found.add(k);
|
|
130
|
+
if (found.size === ALL_KEYWORDS.length)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return found;
|
|
134
|
+
}
|
|
135
|
+
function scrubSecrets(input, opts = {}) {
|
|
87
136
|
if (!input)
|
|
88
137
|
return { text: input, redactions: [] };
|
|
89
138
|
const candidates = [];
|
|
90
|
-
|
|
139
|
+
const fallbacks = [];
|
|
140
|
+
const generic = [];
|
|
141
|
+
const extra = (opts.extraRules ?? []).map((r) => ({ name: r.name, re: new RegExp(r.re.source, r.re.flags.includes("g") ? r.re.flags : r.re.flags + "g"), validate: notTestValue }));
|
|
142
|
+
const present = keywordsIn(input);
|
|
143
|
+
for (const rule of extra.length ? [...RULES, ...extra] : RULES) {
|
|
144
|
+
if (rule.keywords?.length && !rule.keywords.some((k) => present.has(k)))
|
|
145
|
+
continue;
|
|
91
146
|
rule.re.lastIndex = 0;
|
|
92
147
|
let m;
|
|
93
148
|
while ((m = rule.re.exec(input)) !== null) {
|
|
94
149
|
let start = m.index;
|
|
95
150
|
let end = m.index + m[0].length;
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
const groupIndex = rule.firstGroup ? m.findIndex((g, i) => i > 0 && g) : rule.group;
|
|
152
|
+
if (rule.firstGroup && groupIndex === -1) {
|
|
153
|
+
if (m[0].length === 0)
|
|
154
|
+
rule.re.lastIndex++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (groupIndex !== undefined && groupIndex > 0) {
|
|
158
|
+
const gv = m[groupIndex];
|
|
98
159
|
if (gv === undefined || gv === "")
|
|
99
160
|
continue;
|
|
100
161
|
if (rule.validate && !rule.validate(gv, m))
|
|
@@ -111,30 +172,90 @@ function scrubSecrets(input) {
|
|
|
111
172
|
continue;
|
|
112
173
|
}
|
|
113
174
|
if (end > start)
|
|
114
|
-
candidates.push({ rule: rule.name, start, end });
|
|
175
|
+
(rule.fallback ? fallbacks : exports.GENERIC_RULES.has(rule.name) ? generic : candidates).push({ rule: rule.name, start, end });
|
|
115
176
|
if (m[0].length === 0)
|
|
116
177
|
rule.re.lastIndex++;
|
|
117
178
|
}
|
|
118
179
|
}
|
|
119
|
-
if (candidates.length === 0)
|
|
180
|
+
if (candidates.length === 0 && fallbacks.length === 0 && generic.length === 0)
|
|
120
181
|
return { text: input, redactions: [] };
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
const pick = (list, taken) => {
|
|
183
|
+
list.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
184
|
+
const out = [];
|
|
185
|
+
let cursor = -1;
|
|
186
|
+
let t = 0;
|
|
187
|
+
for (const c of list) {
|
|
188
|
+
if (c.start < cursor)
|
|
189
|
+
continue;
|
|
190
|
+
while (t < taken.length && taken[t].end <= c.start)
|
|
191
|
+
t++;
|
|
192
|
+
if (t < taken.length && taken[t].start < c.end)
|
|
193
|
+
continue;
|
|
194
|
+
out.push(c);
|
|
127
195
|
cursor = c.end;
|
|
128
196
|
}
|
|
197
|
+
return out;
|
|
198
|
+
};
|
|
199
|
+
let chosen = pick(candidates, []);
|
|
200
|
+
for (const tier of [fallbacks, generic]) {
|
|
201
|
+
if (tier.length)
|
|
202
|
+
chosen = [...chosen, ...pick(tier, chosen)].sort((a, b) => a.start - b.start);
|
|
129
203
|
}
|
|
204
|
+
return { text: redactSpans(input, chosen), redactions: chosen };
|
|
205
|
+
}
|
|
206
|
+
function redactSpans(input, spans) {
|
|
130
207
|
const parts = [];
|
|
131
208
|
let last = 0;
|
|
132
|
-
for (const c of
|
|
209
|
+
for (const c of spans) {
|
|
133
210
|
parts.push(input.slice(last, c.start), `[REDACTED:${c.rule}]`);
|
|
134
211
|
last = c.end;
|
|
135
212
|
}
|
|
136
213
|
parts.push(input.slice(last));
|
|
137
|
-
return
|
|
214
|
+
return parts.join("");
|
|
215
|
+
}
|
|
216
|
+
exports.GENERIC_RULES = new Set(["secret-assignment", "bearer-token", "jwt"]);
|
|
217
|
+
const SEQUENTIAL = /0123456|1234567|2345678|3456789|abcdefg|bcdefgh|qwertyu/i;
|
|
218
|
+
const WORDISH = /^[A-Za-z]{2,}$/;
|
|
219
|
+
function looksLikeTestValue(value) {
|
|
220
|
+
if (SEQUENTIAL.test(value))
|
|
221
|
+
return true;
|
|
222
|
+
if (/(.)\1{9,}$/.test(value))
|
|
223
|
+
return true;
|
|
224
|
+
const parts = value.split(/[_-]+/).filter(Boolean);
|
|
225
|
+
if (parts.length < 3)
|
|
226
|
+
return false;
|
|
227
|
+
const words = parts.filter((p) => WORDISH.test(p)).length;
|
|
228
|
+
const filler = parts.filter((p) => /^\d+$/.test(p) || /^(.)\1*$/.test(p)).length;
|
|
229
|
+
return words >= 2 && words + filler === parts.length;
|
|
230
|
+
}
|
|
231
|
+
const TEST_FILE = /(?:\.test\.|\.spec\.|__tests__|(?:^|\/)tests?\/|fixtures?\/|__mocks__)/i;
|
|
232
|
+
const NAME_BEFORE = /([A-Za-z_][A-Za-z0-9_.-]{0,63})\??["']?\s*[:=]\s*\??["']?(?:Bearer\s+)?$/;
|
|
233
|
+
function nameBefore(input, start, lineStart) {
|
|
234
|
+
const window = input.slice(Math.max(lineStart, start - 160), start).replace(/\\[nrt]/g, " ");
|
|
235
|
+
const m = window.match(NAME_BEFORE)?.[1];
|
|
236
|
+
if (!m || /^(?:https?|Bearer|Authorization)$/i.test(m))
|
|
237
|
+
return m === "Authorization" ? m : undefined;
|
|
238
|
+
return m;
|
|
239
|
+
}
|
|
240
|
+
const FILE_PATH_FIELD = /"(?:file_path|filePath|path|target_file)"\s*:\s*"([^"]{1,400})"/g;
|
|
241
|
+
const REDIRECT = /(?:>>?|\btee(?:\s+-a)?)\s*['"]?([^\s'"<>|;&()]{2,300})/g;
|
|
242
|
+
const CONFIG_FILE = /((?:[\w.~-]+\/)*\.env(?:\.[\w-]+)?|(?:[\w.~-]+\/)*[\w.-]+\.(?:env|ya?ml|toml|ini|cfg|conf|properties|tfvars|npmrc|netrc|pgpass))(?![\w/])/g;
|
|
243
|
+
const lastTwo = (p) => p.replace(/\\\//g, "/").split("/").filter(Boolean).slice(-2).join("/");
|
|
244
|
+
function lastMatch(re, text) {
|
|
245
|
+
let last;
|
|
246
|
+
for (const m of text.matchAll(re))
|
|
247
|
+
last = m[1];
|
|
248
|
+
return last;
|
|
249
|
+
}
|
|
250
|
+
function fileOf(input, start, lineStart) {
|
|
251
|
+
const lineEnd = input.indexOf("\n", start);
|
|
252
|
+
const line = input.slice(lineStart, lineEnd === -1 ? input.length : lineEnd);
|
|
253
|
+
const before = input.slice(Math.max(lineStart, start - 400), start);
|
|
254
|
+
const hit = lastMatch(FILE_PATH_FIELD, line) ?? lastMatch(REDIRECT, before) ?? lastMatch(CONFIG_FILE, before);
|
|
255
|
+
if (!hit || hit.startsWith("/dev/") || hit === "&1" || hit === "&2")
|
|
256
|
+
return undefined;
|
|
257
|
+
const short = lastTwo(hit);
|
|
258
|
+
return short && short.length <= 120 ? short : undefined;
|
|
138
259
|
}
|
|
139
260
|
const TOOL_CALL_TYPE = /^(?:tool_use|function_call|custom_tool_call|tool_call|toolCall|tool_request|tool\.execution_start|server_tool_use|mcp_tool_use)$/;
|
|
140
261
|
const TOOL_OUTPUT_TYPE = /^(?:tool_result|function_call_output|custom_tool_call_output|tool_output|toolResult|tool\.execution_complete|mcp_tool_result)$/;
|
|
@@ -203,7 +324,7 @@ function locate(input, r, cache) {
|
|
|
203
324
|
return { where: "unknown" };
|
|
204
325
|
}
|
|
205
326
|
const MAX_LOCATIONS = 8;
|
|
206
|
-
function secretFindings(input, redactions) {
|
|
327
|
+
function secretFindings(input, redactions, opts = {}) {
|
|
207
328
|
const seen = new Map();
|
|
208
329
|
const cache = new Map();
|
|
209
330
|
for (const r of redactions) {
|
|
@@ -213,8 +334,21 @@ function secretFindings(input, redactions) {
|
|
|
213
334
|
const hint = value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
|
|
214
335
|
if (!hint)
|
|
215
336
|
continue;
|
|
337
|
+
const lineStart = input.lastIndexOf("\n", r.start - 1) + 1;
|
|
338
|
+
const name = nameBefore(input, r.start, lineStart);
|
|
339
|
+
const file = fileOf(input, r.start, lineStart);
|
|
340
|
+
if (looksLikeTestValue(value))
|
|
341
|
+
continue;
|
|
342
|
+
if (exports.GENERIC_RULES.has(r.rule) && file && TEST_FILE.test(file))
|
|
343
|
+
continue;
|
|
344
|
+
if (opts.skip?.({ rule: r.rule, value, ...(name ? { name } : {}), ...(file ? { file } : {}) }))
|
|
345
|
+
continue;
|
|
216
346
|
const key = `${r.rule}\u0000${hint}`;
|
|
217
347
|
const finding = seen.get(key) ?? { rule: r.rule, hint, locations: [] };
|
|
348
|
+
if (name && !finding.name)
|
|
349
|
+
finding.name = name;
|
|
350
|
+
if (file && !finding.file)
|
|
351
|
+
finding.file = file;
|
|
218
352
|
seen.set(key, finding);
|
|
219
353
|
const loc = locate(input, r, cache);
|
|
220
354
|
if (finding.locations.length < MAX_LOCATIONS && !finding.locations.some((l) => l.where === loc.where && l.tool === loc.tool)) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROVIDER_RULE_LABELS = exports.PROVIDER_RULES = void 0;
|
|
4
|
+
exports.PROVIDER_RULES = [
|
|
5
|
+
{ name: "1password-secret-key", re: new RegExp("\\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\\b", "g"), entropy: 3.8, keywords: ["a3-"] },
|
|
6
|
+
{ name: "1password-service-account-token", re: new RegExp("ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}", "g"), entropy: 4, keywords: ["ops_"] },
|
|
7
|
+
{ name: "adobe-client-secret", re: new RegExp("\\b(p8e-[a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["p8e-"] },
|
|
8
|
+
{ name: "age-secret-key", re: new RegExp("AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}", "g"), keywords: ["age-secret-key-1"] },
|
|
9
|
+
{ name: "airtable-personnal-access-token", re: new RegExp("\\b(pat[A-Za-z0-9]{14}\\.[a-f0-9]{64})\\b", "g"), firstGroup: true, keywords: ["airtable"] },
|
|
10
|
+
{ name: "alibaba-access-key-id", re: new RegExp("\\b(LTAI[a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["ltai"] },
|
|
11
|
+
{ name: "artifactory-api-key", re: new RegExp("\\bAKCp[A-Za-z0-9]{69}\\b", "g"), entropy: 4.5, keywords: ["akcp"] },
|
|
12
|
+
{ name: "artifactory-reference-token", re: new RegExp("\\bcmVmd[A-Za-z0-9]{59}\\b", "g"), entropy: 4.5, keywords: ["cmvmd"] },
|
|
13
|
+
{ name: "clickhouse-cloud-api-secret-key", re: new RegExp("\\b(4b1d[A-Za-z0-9]{38})\\b", "g"), firstGroup: true, entropy: 3, keywords: ["4b1d"] },
|
|
14
|
+
{ name: "clojars-api-token", re: new RegExp("CLOJARS_[a-z0-9]{60}", "gi"), entropy: 2, keywords: ["clojars_"] },
|
|
15
|
+
{ name: "cloudflare-origin-ca-key", re: new RegExp("\\b(v1\\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["cloudflare", "v1.0-"] },
|
|
16
|
+
{ name: "curl-auth-header", re: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|$)", "gi"), firstGroup: true, entropy: 2.75, keywords: ["curl"] },
|
|
17
|
+
{ name: "curl-auth-user", re: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|$)", "g"), firstGroup: true, entropy: 2, keywords: ["curl"] },
|
|
18
|
+
{ name: "databricks-api-token", re: new RegExp("\\b(dapi[a-f0-9]{32}(?:-\\d)?)(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["dapi"] },
|
|
19
|
+
{ name: "digitalocean-access-token", re: new RegExp("\\b(doo_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["doo_v1_"] },
|
|
20
|
+
{ name: "digitalocean-pat", re: new RegExp("\\b(dop_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["dop_v1_"] },
|
|
21
|
+
{ name: "digitalocean-refresh-token", re: new RegExp("\\b(dor_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, keywords: ["dor_v1_"] },
|
|
22
|
+
{ name: "doppler-api-token", re: new RegExp("dp\\.pt\\.[a-z0-9]{43}", "gi"), entropy: 2, keywords: ["dp.pt."] },
|
|
23
|
+
{ name: "duffel-api-token", re: new RegExp("duffel_(?:test|live)_[a-z0-9_\\-=]{43}", "gi"), entropy: 2, keywords: ["duffel_"] },
|
|
24
|
+
{ name: "dynatrace-api-token", re: new RegExp("dt0c01\\.[a-z0-9]{24}\\.[a-z0-9]{64}", "gi"), entropy: 4, keywords: ["dt0c01."] },
|
|
25
|
+
{ name: "easypost-api-token", re: new RegExp("\\bEZAK[a-z0-9]{54}\\b", "gi"), entropy: 2, keywords: ["ezak"] },
|
|
26
|
+
{ name: "easypost-test-api-token", re: new RegExp("\\bEZTK[a-z0-9]{54}\\b", "gi"), entropy: 2, keywords: ["eztk"] },
|
|
27
|
+
{ name: "facebook-access-token", re: new RegExp("\\b(\\d{15,16}(\\||%)[0-9a-z\\-_]{27,40})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["facebook"] },
|
|
28
|
+
{ name: "facebook-page-access-token", re: new RegExp("\\b(EAA[MC][a-z0-9]{100,})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 4, keywords: ["eaam", "eaac"] },
|
|
29
|
+
{ name: "flutterwave-encryption-key", re: new RegExp("FLWSECK_TEST-[a-h0-9]{12}", "gi"), entropy: 2, keywords: ["flwseck_test"] },
|
|
30
|
+
{ name: "flutterwave-secret-key", re: new RegExp("FLWSECK_TEST-[a-h0-9]{32}-X", "gi"), entropy: 2, keywords: ["flwseck_test"] },
|
|
31
|
+
{ name: "flyio-access-token", re: new RegExp("\\b((?:fo1_[\\w-]{43}|fm1[ar]_[a-zA-Z0-9+\\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\\/]{100,}={0,3}))(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["fo1_", "fm1", "fm2_"] },
|
|
32
|
+
{ name: "frameio-api-token", re: new RegExp("fio-u-[a-z0-9\\-_=]{64}", "gi"), keywords: ["fio-u-"] },
|
|
33
|
+
{ name: "grafana-api-key", re: new RegExp("\\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["eyjrijoi"] },
|
|
34
|
+
{ name: "grafana-cloud-api-token", re: new RegExp("\\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["glc_"] },
|
|
35
|
+
{ name: "grafana-service-account-token", re: new RegExp("\\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["glsa_"] },
|
|
36
|
+
{ name: "harness-api-key", re: new RegExp("(?:pat|sat)\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9]{24}\\.[a-zA-Z0-9]{20}", "g"), keywords: ["pat.", "sat."] },
|
|
37
|
+
{ name: "heroku-api-key-v2", re: new RegExp("\\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["hrku-aa"] },
|
|
38
|
+
{ name: "huggingface-access-token", re: new RegExp("\\b(hf_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["hf_"] },
|
|
39
|
+
{ name: "huggingface-organization-api-token", re: new RegExp("\\b(api_org_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["api_org_"] },
|
|
40
|
+
{ name: "infracost-api-token", re: new RegExp("\\b(ico-[a-zA-Z0-9]{32})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["ico-"] },
|
|
41
|
+
{ name: "intra42-client-secret", re: new RegExp("\\b(s-s4t2(?:ud|af)-[abcdef0123456789]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["intra", "s-s4t2ud-", "s-s4t2af-"] },
|
|
42
|
+
{ name: "maxmind-license-key", re: new RegExp("\\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["_mmk"] },
|
|
43
|
+
{ name: "microsoft-teams-webhook", re: new RegExp("https://[a-z0-9]+\\.webhook\\.office\\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}", "g"), firstGroup: true, keywords: ["webhook.office.com", "webhookb2", "incomingwebhook"] },
|
|
44
|
+
{ name: "notion-api-token", re: new RegExp("\\b(ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["ntn_"] },
|
|
45
|
+
{ name: "octopus-deploy-api-key", re: new RegExp("\\b(API-[A-Z0-9]{26})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["api-"] },
|
|
46
|
+
{ name: "openshift-user-token", re: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|$)", "g"), firstGroup: true, entropy: 3.5, keywords: ["sha256~"] },
|
|
47
|
+
{ name: "perplexity-api-key", re: new RegExp("\\b(pplx-[a-zA-Z0-9]{48})(?:[\\x60'\"\\s;]|\\\\|$|\\b)", "g"), firstGroup: true, entropy: 4, keywords: ["pplx-"] },
|
|
48
|
+
{ name: "planetscale-api-token", re: new RegExp("\\b(pscale_tkn_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pscale_tkn_"] },
|
|
49
|
+
{ name: "planetscale-oauth-token", re: new RegExp("\\b(pscale_oauth_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["pscale_oauth_"] },
|
|
50
|
+
{ name: "planetscale-password", re: new RegExp("\\b(pscale_pw_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pscale_pw_"] },
|
|
51
|
+
{ name: "postman-api-token", re: new RegExp("\\b(PMAK-[a-f0-9]{24}\\-[a-f0-9]{34})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pmak-"] },
|
|
52
|
+
{ name: "prefect-api-token", re: new RegExp("\\b(pnu_[a-zA-Z0-9]{36})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["pnu_"] },
|
|
53
|
+
{ name: "pulumi-api-token", re: new RegExp("\\b(pul-[a-f0-9]{40})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["pul-"] },
|
|
54
|
+
{ name: "pypi-upload-token", re: new RegExp("pypi-AgEIcHlwaS5vcmc[\\w-]{50,1000}", "g"), entropy: 3, keywords: ["pypi-ageichlwas5vcmc"] },
|
|
55
|
+
{ name: "readme-api-token", re: new RegExp("\\b(rdme_[a-z0-9]{70})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["rdme_"] },
|
|
56
|
+
{ name: "rubygems-api-token", re: new RegExp("\\b(rubygems_[a-f0-9]{48})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["rubygems_"] },
|
|
57
|
+
{ name: "scalingo-api-token", re: new RegExp("\\b(tk-us-[\\w-]{48})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["tk-us-"] },
|
|
58
|
+
{ name: "sendinblue-api-token", re: new RegExp("\\b(xkeysib-[a-f0-9]{64}\\-[a-z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["xkeysib-"] },
|
|
59
|
+
{ name: "sentry-org-token", re: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|$)", "g"), entropy: 4.5, keywords: ["sntrys_eyjpyxqio"] },
|
|
60
|
+
{ name: "sentry-user-token", re: new RegExp("\\b(sntryu_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3.5, keywords: ["sntryu_"] },
|
|
61
|
+
{ name: "settlemint-application-access-token", re: new RegExp("\\b(sm_aat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_aat"] },
|
|
62
|
+
{ name: "settlemint-personal-access-token", re: new RegExp("\\b(sm_pat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_pat"] },
|
|
63
|
+
{ name: "settlemint-service-access-token", re: new RegExp("\\b(sm_sat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_sat"] },
|
|
64
|
+
{ name: "shippo-api-token", re: new RegExp("\\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["shippo_"] },
|
|
65
|
+
{ name: "shopify-access-token", re: new RegExp("shpat_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpat_"] },
|
|
66
|
+
{ name: "shopify-custom-access-token", re: new RegExp("shpca_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpca_"] },
|
|
67
|
+
{ name: "shopify-private-app-access-token", re: new RegExp("shppa_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shppa_"] },
|
|
68
|
+
{ name: "shopify-shared-secret", re: new RegExp("shpss_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpss_"] },
|
|
69
|
+
{ name: "sidekiq-sensitive-url", re: new RegExp("\\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\\/|\\#|\\?|:]|$)", "gi"), firstGroup: true, keywords: ["gems.contribsys.com", "enterprise.contribsys.com"] },
|
|
70
|
+
{ name: "sourcegraph-access-token", re: new RegExp("\\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40})\\b", "g"), firstGroup: true, entropy: 3, keywords: ["sgp_", "sourcegraph"] },
|
|
71
|
+
{ name: "square-access-token", re: new RegExp("\\b((?:EAAA|sq0atp-)[\\w-]{22,60})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["sq0atp-", "eaaa"] },
|
|
72
|
+
{ name: "vault-batch-token", re: new RegExp("\\b(hvb\\.[\\w-]{138,300})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["hvb."] },
|
|
73
|
+
{ name: "vault-service-token", re: new RegExp("\\b(hvs\\.[\\w-]{90,120})", "g"), firstGroup: true, entropy: 3.5, keywords: ["hvs."] },
|
|
74
|
+
];
|
|
75
|
+
exports.PROVIDER_RULE_LABELS = {
|
|
76
|
+
"1password-secret-key": "1password secret key",
|
|
77
|
+
"1password-service-account-token": "1password service account token",
|
|
78
|
+
"adobe-client-secret": "Adobe client secret",
|
|
79
|
+
"age-secret-key": "Age secret key",
|
|
80
|
+
"airtable-personnal-access-token": "Airtable personnal access token",
|
|
81
|
+
"alibaba-access-key-id": "Alibaba access key id",
|
|
82
|
+
"artifactory-api-key": "Artifactory api key",
|
|
83
|
+
"artifactory-reference-token": "Artifactory reference token",
|
|
84
|
+
"clickhouse-cloud-api-secret-key": "Clickhouse cloud api secret key",
|
|
85
|
+
"clojars-api-token": "Clojars api token",
|
|
86
|
+
"cloudflare-origin-ca-key": "Cloudflare origin ca key",
|
|
87
|
+
"curl-auth-header": "Curl auth header",
|
|
88
|
+
"curl-auth-user": "Curl auth user",
|
|
89
|
+
"databricks-api-token": "Databricks api token",
|
|
90
|
+
"digitalocean-access-token": "Digitalocean access token",
|
|
91
|
+
"digitalocean-pat": "Digitalocean pat",
|
|
92
|
+
"digitalocean-refresh-token": "Digitalocean refresh token",
|
|
93
|
+
"doppler-api-token": "Doppler api token",
|
|
94
|
+
"duffel-api-token": "Duffel api token",
|
|
95
|
+
"dynatrace-api-token": "Dynatrace api token",
|
|
96
|
+
"easypost-api-token": "Easypost api token",
|
|
97
|
+
"easypost-test-api-token": "Easypost test api token",
|
|
98
|
+
"facebook-access-token": "Facebook access token",
|
|
99
|
+
"facebook-page-access-token": "Facebook page access token",
|
|
100
|
+
"flutterwave-encryption-key": "Flutterwave encryption key",
|
|
101
|
+
"flutterwave-secret-key": "Flutterwave secret key",
|
|
102
|
+
"flyio-access-token": "Flyio access token",
|
|
103
|
+
"frameio-api-token": "Frameio api token",
|
|
104
|
+
"grafana-api-key": "Grafana api key",
|
|
105
|
+
"grafana-cloud-api-token": "Grafana cloud api token",
|
|
106
|
+
"grafana-service-account-token": "Grafana service account token",
|
|
107
|
+
"harness-api-key": "Harness api key",
|
|
108
|
+
"heroku-api-key-v2": "Heroku api key v2",
|
|
109
|
+
"huggingface-access-token": "Huggingface access token",
|
|
110
|
+
"huggingface-organization-api-token": "Huggingface organization api token",
|
|
111
|
+
"infracost-api-token": "Infracost api token",
|
|
112
|
+
"intra42-client-secret": "Intra42 client secret",
|
|
113
|
+
"maxmind-license-key": "Maxmind license key",
|
|
114
|
+
"microsoft-teams-webhook": "Microsoft teams webhook",
|
|
115
|
+
"notion-api-token": "Notion api token",
|
|
116
|
+
"octopus-deploy-api-key": "Octopus deploy api key",
|
|
117
|
+
"openshift-user-token": "Openshift user token",
|
|
118
|
+
"perplexity-api-key": "Perplexity api key",
|
|
119
|
+
"planetscale-api-token": "Planetscale api token",
|
|
120
|
+
"planetscale-oauth-token": "Planetscale oauth token",
|
|
121
|
+
"planetscale-password": "Planetscale password",
|
|
122
|
+
"postman-api-token": "Postman api token",
|
|
123
|
+
"prefect-api-token": "Prefect api token",
|
|
124
|
+
"pulumi-api-token": "Pulumi api token",
|
|
125
|
+
"pypi-upload-token": "Pypi upload token",
|
|
126
|
+
"readme-api-token": "Readme api token",
|
|
127
|
+
"rubygems-api-token": "Rubygems api token",
|
|
128
|
+
"scalingo-api-token": "Scalingo api token",
|
|
129
|
+
"sendinblue-api-token": "Sendinblue api token",
|
|
130
|
+
"sentry-org-token": "Sentry org token",
|
|
131
|
+
"sentry-user-token": "Sentry user token",
|
|
132
|
+
"settlemint-application-access-token": "Settlemint application access token",
|
|
133
|
+
"settlemint-personal-access-token": "Settlemint personal access token",
|
|
134
|
+
"settlemint-service-access-token": "Settlemint service access token",
|
|
135
|
+
"shippo-api-token": "Shippo api token",
|
|
136
|
+
"shopify-access-token": "Shopify access token",
|
|
137
|
+
"shopify-custom-access-token": "Shopify custom access token",
|
|
138
|
+
"shopify-private-app-access-token": "Shopify private app access token",
|
|
139
|
+
"shopify-shared-secret": "Shopify shared secret",
|
|
140
|
+
"sidekiq-sensitive-url": "Sidekiq sensitive url",
|
|
141
|
+
"sourcegraph-access-token": "Sourcegraph access token",
|
|
142
|
+
"square-access-token": "Square access token",
|
|
143
|
+
"vault-batch-token": "Vault batch token",
|
|
144
|
+
"vault-service-token": "Vault service token",
|
|
145
|
+
};
|
package/dist/utils.js
CHANGED
|
@@ -1421,7 +1421,6 @@ but does not install a global \`codiedev\` executable.
|
|
|
1421
1421
|
| "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
|
|
1422
1422
|
| "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
|
|
1423
1423
|
| "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
|
|
1424
|
-
| "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
|
|
1425
1424
|
| "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
|
|
1426
1425
|
| "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
|
|
1427
1426
|
| "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
|
|
@@ -1470,11 +1469,6 @@ iterate rather than duplicating.
|
|
|
1470
1469
|
notification). \`codiedev send\` both grants access AND pings the
|
|
1471
1470
|
recipient. Use send when the user is actively looping someone in.
|
|
1472
1471
|
|
|
1473
|
-
**\`create-ticket\` flow:** when invoked without \`--submit\`, the CLI prints
|
|
1474
|
-
skill instructions you should follow. Step 1 is asking the user about repos.
|
|
1475
|
-
Step 4 calls \`codiedev create-ticket --submit <path>\` with the plan JSON.
|
|
1476
|
-
Tell the user the portal URL when the submit returns.
|
|
1477
|
-
|
|
1478
1472
|
**Errors:**
|
|
1479
1473
|
- "not connected" → user needs to run \`npx codiedev connect\` with their
|
|
1480
1474
|
API token from https://codiedev.com/portal/integrations/claude-code
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.16",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"prepublishOnly": "tsc",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
|
|
16
|
-
"test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
|
|
15
|
+
"test:functional": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
|
|
16
|
+
"test:harness-parity": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
|
|
17
17
|
"test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
|
|
18
18
|
"test:opencode-attribution": "vitest run src/__tests__/repoResolver.test.ts src/__tests__/openCodeCapture.test.ts src/__tests__/openCodePluginGitRefs.test.ts src/__tests__/capturePayloadFinal.test.ts src/__tests__/captureDirectUpload.test.ts src/__tests__/resolveCaptureFinal.test.ts && tsc",
|
|
19
19
|
"test:watch": "vitest",
|
|
20
|
-
"test:metadata-parity": "vitest run src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/piCapture.functional.test.ts src/__tests__/piHooks.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts",
|
|
20
|
+
"test:metadata-parity": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/piCapture.functional.test.ts src/__tests__/piHooks.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts",
|
|
21
21
|
"test:local-scrub-live": "npm run build && node scripts/localScrubLive.mjs"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.substitutePrompt = substitutePrompt;
|
|
37
|
-
exports.validatePlanHasRepo = validatePlanHasRepo;
|
|
38
|
-
exports.resolveRepoIds = resolveRepoIds;
|
|
39
|
-
exports.parseCreateTicketArgs = parseCreateTicketArgs;
|
|
40
|
-
exports.runCreateTicket = runCreateTicket;
|
|
41
|
-
exports.runBare = runBare;
|
|
42
|
-
exports.runSubmit = runSubmit;
|
|
43
|
-
exports.readSubmitInput = readSubmitInput;
|
|
44
|
-
const fs = __importStar(require("node:fs"));
|
|
45
|
-
const shared_1 = require("./shared");
|
|
46
|
-
const PROMPT_PLACEHOLDER = "{{USER_PROMPT}}";
|
|
47
|
-
const MISSING_PROMPT_FALLBACK = "(not yet provided — ask the user a single combined question that covers both the intent AND repo locations: \"What are you working on, and is this in a single repo or multiple? If multiple, give me the paths.\" Skip Step 1 since you've folded it into this question. Then proceed to Step 2.)";
|
|
48
|
-
function substitutePrompt(protocol, prompt) {
|
|
49
|
-
return protocol.replace(PROMPT_PLACEHOLDER, prompt && prompt.length > 0 ? prompt : MISSING_PROMPT_FALLBACK);
|
|
50
|
-
}
|
|
51
|
-
function validatePlanHasRepo(parsedPlan) {
|
|
52
|
-
const fullNames = Array.isArray(parsedPlan?.repoFullNames) ? parsedPlan.repoFullNames : [];
|
|
53
|
-
const explicitIds = Array.isArray(parsedPlan?.repoIds) ? parsedPlan.repoIds : [];
|
|
54
|
-
if (fullNames.length === 0 && explicitIds.length === 0) {
|
|
55
|
-
return "Plan must include at least one repo (repoFullNames: ['owner/name'] or repoIds: [...])";
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
function resolveRepoIds(fullNames, repos) {
|
|
60
|
-
const lookup = new Map();
|
|
61
|
-
for (const r of repos) {
|
|
62
|
-
lookup.set(r.fullName.toLowerCase(), r.repoId);
|
|
63
|
-
}
|
|
64
|
-
const ids = [];
|
|
65
|
-
for (const fn of fullNames) {
|
|
66
|
-
const id = lookup.get(fn.toLowerCase());
|
|
67
|
-
if (!id) {
|
|
68
|
-
throw new Error(`Repo "${fn}" is not connected to your Codiedev account. Run \`codiedev connect\` to refresh, or push the missing repo through Settings.`);
|
|
69
|
-
}
|
|
70
|
-
ids.push(id);
|
|
71
|
-
}
|
|
72
|
-
return ids;
|
|
73
|
-
}
|
|
74
|
-
function parseCreateTicketArgs(args) {
|
|
75
|
-
let mode = "bare";
|
|
76
|
-
let prompt;
|
|
77
|
-
let submitSource;
|
|
78
|
-
for (let i = 0; i < args.length; i++) {
|
|
79
|
-
const a = args[i];
|
|
80
|
-
if (a === "--submit" && i + 1 < args.length) {
|
|
81
|
-
mode = "submit";
|
|
82
|
-
const next = args[++i];
|
|
83
|
-
submitSource = next === "-" ? "stdin" : next;
|
|
84
|
-
}
|
|
85
|
-
else if (a.startsWith("--submit=")) {
|
|
86
|
-
mode = "submit";
|
|
87
|
-
const v = a.slice("--submit=".length);
|
|
88
|
-
submitSource = v === "-" ? "stdin" : v;
|
|
89
|
-
}
|
|
90
|
-
else if (!a.startsWith("--") && !prompt && mode === "bare") {
|
|
91
|
-
prompt = a;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return { mode, prompt, submitSource };
|
|
95
|
-
}
|
|
96
|
-
async function runCreateTicket(args) {
|
|
97
|
-
const parsed = parseCreateTicketArgs(args);
|
|
98
|
-
if (parsed.mode === "bare") {
|
|
99
|
-
const config = (0, shared_1.requireConfig)();
|
|
100
|
-
await runBare(config, parsed.prompt);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (!parsed.submitSource) {
|
|
104
|
-
console.error("--submit requires a path or '-' for stdin");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
const config = (0, shared_1.requireConfig)();
|
|
108
|
-
let raw;
|
|
109
|
-
try {
|
|
110
|
-
raw = readSubmitInput(parsed.submitSource);
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
console.error(`Failed to read plan: ${err.message}`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
let parsedPlan;
|
|
117
|
-
try {
|
|
118
|
-
parsedPlan = JSON.parse(raw);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
console.error(`Plan JSON did not parse: ${err.message}`);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const validationError = validatePlanHasRepo(parsedPlan);
|
|
125
|
-
if (validationError) {
|
|
126
|
-
console.error(validationError);
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
const fullNames = Array.isArray(parsedPlan.repoFullNames) ? parsedPlan.repoFullNames : [];
|
|
130
|
-
const explicitIds = Array.isArray(parsedPlan.repoIds) ? parsedPlan.repoIds : [];
|
|
131
|
-
if (fullNames.length > 0 && explicitIds.length === 0) {
|
|
132
|
-
parsedPlan.repoIds = resolveRepoIds(fullNames, config.repos);
|
|
133
|
-
}
|
|
134
|
-
for (const t of parsedPlan.plan?.tickets ?? []) {
|
|
135
|
-
if (t.targetRepoFullName && !t.targetRepoId) {
|
|
136
|
-
const ids = resolveRepoIds([t.targetRepoFullName], config.repos);
|
|
137
|
-
t.targetRepoId = ids[0];
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
await runSubmit(config, JSON.stringify(parsedPlan));
|
|
141
|
-
}
|
|
142
|
-
async function runBare(config, prompt) {
|
|
143
|
-
let res;
|
|
144
|
-
try {
|
|
145
|
-
res = await (0, shared_1.apiRequest)("GET", "/api/cli/createTicketProtocol", { config });
|
|
146
|
-
}
|
|
147
|
-
catch (err) {
|
|
148
|
-
console.error(`Failed to fetch create-ticket protocol: ${err.message}. Check your network connection and re-run.`);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
process.stdout.write(substitutePrompt(res.protocol, prompt));
|
|
152
|
-
}
|
|
153
|
-
async function runSubmit(config, rawJson) {
|
|
154
|
-
let payload;
|
|
155
|
-
try {
|
|
156
|
-
payload = JSON.parse(rawJson);
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
throw new Error(`Plan JSON did not parse — emit valid JSON matching the schema printed by \`codiedev create-ticket\`. Original error: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
if (!payload.plan || !payload.prompt || !payload.repoIds) {
|
|
162
|
-
throw new Error("Plan JSON missing one of: plan, prompt, repoIds");
|
|
163
|
-
}
|
|
164
|
-
const res = await (0, shared_1.withTelemetry)(config, "codiedev_create_ticket", () => (0, shared_1.apiRequest)("POST", "/api/cli/createPlan", {
|
|
165
|
-
config,
|
|
166
|
-
body: payload,
|
|
167
|
-
}));
|
|
168
|
-
const portalHost = (process.env.CODIEDEV_PORTAL_URL || "https://codiedev.com").replace(/\/$/, "");
|
|
169
|
-
const portalLink = res.portalUrl.startsWith("http")
|
|
170
|
-
? res.portalUrl
|
|
171
|
-
: `${portalHost}${res.portalUrl}`;
|
|
172
|
-
console.log(`✓ Created ${res.ticketCount} ticket(s).`);
|
|
173
|
-
console.log(` portal: ${portalLink}`);
|
|
174
|
-
console.log("");
|
|
175
|
-
console.log("Open in your portal to review and push to Jira.");
|
|
176
|
-
}
|
|
177
|
-
function readSubmitInput(source) {
|
|
178
|
-
if (source === "stdin") {
|
|
179
|
-
return fs.readFileSync(0, "utf8");
|
|
180
|
-
}
|
|
181
|
-
return fs.readFileSync(source, "utf8");
|
|
182
|
-
}
|