@pretense/cli 0.3.0
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/README.md +57 -0
- package/bin/pretense +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1581 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1581 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync, statSync } from "fs";
|
|
7
|
+
import { resolve as resolve2, extname, join as join3 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/types.ts
|
|
10
|
+
var DEFAULT_CONFIG = {
|
|
11
|
+
port: 9339,
|
|
12
|
+
targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
|
|
13
|
+
languages: ["typescript", "javascript", "python", "go", "java"],
|
|
14
|
+
mutationRules: [
|
|
15
|
+
{ tokenType: "Variable" /* Variable */, prefix: "_v" },
|
|
16
|
+
{ tokenType: "Function" /* Function */, prefix: "_fn" },
|
|
17
|
+
{ tokenType: "Class" /* Class */, prefix: "_cls" },
|
|
18
|
+
{ tokenType: "String" /* String */ },
|
|
19
|
+
{ tokenType: "Comment" /* Comment */, strip: true },
|
|
20
|
+
{ tokenType: "Import" /* Import */ },
|
|
21
|
+
{ tokenType: "Property" /* Property */, prefix: "_prop" },
|
|
22
|
+
{ tokenType: "Method" /* Method */, prefix: "_fn" }
|
|
23
|
+
],
|
|
24
|
+
storePath: `${process.env["HOME"] ?? "/tmp"}/.pretense`
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/scanner.ts
|
|
28
|
+
var TS_JS_KEYWORDS = /* @__PURE__ */ new Set([
|
|
29
|
+
"abstract",
|
|
30
|
+
"any",
|
|
31
|
+
"as",
|
|
32
|
+
"async",
|
|
33
|
+
"await",
|
|
34
|
+
"boolean",
|
|
35
|
+
"break",
|
|
36
|
+
"case",
|
|
37
|
+
"catch",
|
|
38
|
+
"class",
|
|
39
|
+
"const",
|
|
40
|
+
"constructor",
|
|
41
|
+
"continue",
|
|
42
|
+
"debugger",
|
|
43
|
+
"declare",
|
|
44
|
+
"default",
|
|
45
|
+
"delete",
|
|
46
|
+
"do",
|
|
47
|
+
"else",
|
|
48
|
+
"enum",
|
|
49
|
+
"export",
|
|
50
|
+
"extends",
|
|
51
|
+
"false",
|
|
52
|
+
"finally",
|
|
53
|
+
"for",
|
|
54
|
+
"from",
|
|
55
|
+
"function",
|
|
56
|
+
"get",
|
|
57
|
+
"if",
|
|
58
|
+
"implements",
|
|
59
|
+
"import",
|
|
60
|
+
"in",
|
|
61
|
+
"instanceof",
|
|
62
|
+
"interface",
|
|
63
|
+
"keyof",
|
|
64
|
+
"let",
|
|
65
|
+
"module",
|
|
66
|
+
"namespace",
|
|
67
|
+
"never",
|
|
68
|
+
"new",
|
|
69
|
+
"null",
|
|
70
|
+
"number",
|
|
71
|
+
"object",
|
|
72
|
+
"of",
|
|
73
|
+
"package",
|
|
74
|
+
"private",
|
|
75
|
+
"protected",
|
|
76
|
+
"public",
|
|
77
|
+
"readonly",
|
|
78
|
+
"require",
|
|
79
|
+
"return",
|
|
80
|
+
"set",
|
|
81
|
+
"static",
|
|
82
|
+
"string",
|
|
83
|
+
"super",
|
|
84
|
+
"switch",
|
|
85
|
+
"symbol",
|
|
86
|
+
"this",
|
|
87
|
+
"throw",
|
|
88
|
+
"true",
|
|
89
|
+
"try",
|
|
90
|
+
"type",
|
|
91
|
+
"typeof",
|
|
92
|
+
"undefined",
|
|
93
|
+
"unknown",
|
|
94
|
+
"var",
|
|
95
|
+
"void",
|
|
96
|
+
"while",
|
|
97
|
+
"with",
|
|
98
|
+
"yield",
|
|
99
|
+
"satisfies",
|
|
100
|
+
"using",
|
|
101
|
+
"infer",
|
|
102
|
+
"asserts"
|
|
103
|
+
]);
|
|
104
|
+
var PYTHON_KEYWORDS = /* @__PURE__ */ new Set([
|
|
105
|
+
"False",
|
|
106
|
+
"None",
|
|
107
|
+
"True",
|
|
108
|
+
"and",
|
|
109
|
+
"as",
|
|
110
|
+
"assert",
|
|
111
|
+
"async",
|
|
112
|
+
"await",
|
|
113
|
+
"break",
|
|
114
|
+
"class",
|
|
115
|
+
"continue",
|
|
116
|
+
"def",
|
|
117
|
+
"del",
|
|
118
|
+
"elif",
|
|
119
|
+
"else",
|
|
120
|
+
"except",
|
|
121
|
+
"finally",
|
|
122
|
+
"for",
|
|
123
|
+
"from",
|
|
124
|
+
"global",
|
|
125
|
+
"if",
|
|
126
|
+
"import",
|
|
127
|
+
"in",
|
|
128
|
+
"is",
|
|
129
|
+
"lambda",
|
|
130
|
+
"nonlocal",
|
|
131
|
+
"not",
|
|
132
|
+
"or",
|
|
133
|
+
"pass",
|
|
134
|
+
"raise",
|
|
135
|
+
"return",
|
|
136
|
+
"try",
|
|
137
|
+
"while",
|
|
138
|
+
"with",
|
|
139
|
+
"yield",
|
|
140
|
+
"self",
|
|
141
|
+
"cls"
|
|
142
|
+
]);
|
|
143
|
+
var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
144
|
+
"break",
|
|
145
|
+
"case",
|
|
146
|
+
"chan",
|
|
147
|
+
"const",
|
|
148
|
+
"continue",
|
|
149
|
+
"default",
|
|
150
|
+
"defer",
|
|
151
|
+
"else",
|
|
152
|
+
"fallthrough",
|
|
153
|
+
"for",
|
|
154
|
+
"func",
|
|
155
|
+
"go",
|
|
156
|
+
"goto",
|
|
157
|
+
"if",
|
|
158
|
+
"import",
|
|
159
|
+
"interface",
|
|
160
|
+
"map",
|
|
161
|
+
"package",
|
|
162
|
+
"range",
|
|
163
|
+
"return",
|
|
164
|
+
"select",
|
|
165
|
+
"struct",
|
|
166
|
+
"switch",
|
|
167
|
+
"type",
|
|
168
|
+
"var",
|
|
169
|
+
"nil",
|
|
170
|
+
"true",
|
|
171
|
+
"false",
|
|
172
|
+
"error",
|
|
173
|
+
"string",
|
|
174
|
+
"int",
|
|
175
|
+
"int8",
|
|
176
|
+
"int16",
|
|
177
|
+
"int32",
|
|
178
|
+
"int64",
|
|
179
|
+
"uint",
|
|
180
|
+
"uint8",
|
|
181
|
+
"uint16",
|
|
182
|
+
"uint32",
|
|
183
|
+
"uint64",
|
|
184
|
+
"float32",
|
|
185
|
+
"float64",
|
|
186
|
+
"complex64",
|
|
187
|
+
"complex128",
|
|
188
|
+
"bool",
|
|
189
|
+
"byte",
|
|
190
|
+
"rune",
|
|
191
|
+
"uintptr",
|
|
192
|
+
"make",
|
|
193
|
+
"new",
|
|
194
|
+
"len",
|
|
195
|
+
"cap",
|
|
196
|
+
"append",
|
|
197
|
+
"copy",
|
|
198
|
+
"close",
|
|
199
|
+
"delete",
|
|
200
|
+
"panic",
|
|
201
|
+
"recover",
|
|
202
|
+
"print",
|
|
203
|
+
"println"
|
|
204
|
+
]);
|
|
205
|
+
var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
206
|
+
"abstract",
|
|
207
|
+
"assert",
|
|
208
|
+
"boolean",
|
|
209
|
+
"break",
|
|
210
|
+
"byte",
|
|
211
|
+
"case",
|
|
212
|
+
"catch",
|
|
213
|
+
"char",
|
|
214
|
+
"class",
|
|
215
|
+
"const",
|
|
216
|
+
"continue",
|
|
217
|
+
"default",
|
|
218
|
+
"do",
|
|
219
|
+
"double",
|
|
220
|
+
"else",
|
|
221
|
+
"enum",
|
|
222
|
+
"extends",
|
|
223
|
+
"final",
|
|
224
|
+
"finally",
|
|
225
|
+
"float",
|
|
226
|
+
"for",
|
|
227
|
+
"goto",
|
|
228
|
+
"if",
|
|
229
|
+
"implements",
|
|
230
|
+
"import",
|
|
231
|
+
"instanceof",
|
|
232
|
+
"int",
|
|
233
|
+
"interface",
|
|
234
|
+
"long",
|
|
235
|
+
"native",
|
|
236
|
+
"new",
|
|
237
|
+
"package",
|
|
238
|
+
"private",
|
|
239
|
+
"protected",
|
|
240
|
+
"public",
|
|
241
|
+
"return",
|
|
242
|
+
"short",
|
|
243
|
+
"static",
|
|
244
|
+
"strictfp",
|
|
245
|
+
"super",
|
|
246
|
+
"switch",
|
|
247
|
+
"synchronized",
|
|
248
|
+
"this",
|
|
249
|
+
"throw",
|
|
250
|
+
"throws",
|
|
251
|
+
"transient",
|
|
252
|
+
"try",
|
|
253
|
+
"void",
|
|
254
|
+
"volatile",
|
|
255
|
+
"while",
|
|
256
|
+
"true",
|
|
257
|
+
"false",
|
|
258
|
+
"null",
|
|
259
|
+
"var",
|
|
260
|
+
"record",
|
|
261
|
+
"sealed",
|
|
262
|
+
"permits",
|
|
263
|
+
"yield"
|
|
264
|
+
]);
|
|
265
|
+
function detectLanguage(code) {
|
|
266
|
+
if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
|
|
267
|
+
if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
|
|
268
|
+
if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
|
|
269
|
+
if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
|
|
270
|
+
if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
|
|
271
|
+
return "typescript";
|
|
272
|
+
}
|
|
273
|
+
function lineAt(code, index) {
|
|
274
|
+
let line = 1;
|
|
275
|
+
for (let i = 0; i < index && i < code.length; i++) {
|
|
276
|
+
if (code[i] === "\n") line++;
|
|
277
|
+
}
|
|
278
|
+
return line;
|
|
279
|
+
}
|
|
280
|
+
function scanTypeScript(code) {
|
|
281
|
+
const tokens = [];
|
|
282
|
+
if (!code.trim()) return tokens;
|
|
283
|
+
const patterns = [
|
|
284
|
+
// Single-line comment
|
|
285
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
286
|
+
// Multi-line comment
|
|
287
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
288
|
+
// Import source (the module path string)
|
|
289
|
+
{ re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
290
|
+
// Class name
|
|
291
|
+
{ re: /\bclass\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
292
|
+
// Function/method name (function keyword)
|
|
293
|
+
{ re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
294
|
+
// Arrow function assigned to const/let/var
|
|
295
|
+
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
296
|
+
// Method: identifier followed by (
|
|
297
|
+
{ re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
298
|
+
// Variable declarations
|
|
299
|
+
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
300
|
+
// Template literals (preserve backtick wrapper)
|
|
301
|
+
{ re: /`[^`]*`/g, type: "String" /* String */ },
|
|
302
|
+
// Double-quoted strings
|
|
303
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
304
|
+
// Single-quoted strings
|
|
305
|
+
{ re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
|
|
306
|
+
];
|
|
307
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
308
|
+
re.lastIndex = 0;
|
|
309
|
+
let m;
|
|
310
|
+
while ((m = re.exec(code)) !== null) {
|
|
311
|
+
const fullMatch = m[0];
|
|
312
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
313
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
314
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
315
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
|
|
316
|
+
if (TS_JS_KEYWORDS.has(captureValue)) continue;
|
|
317
|
+
if (/^(console|process|Object|Array|Math|JSON|Promise|Error|Map|Set|Date|RegExp|Symbol|Proxy|Reflect|global|globalThis|window|document|module|exports|require|__dirname|__filename)$/.test(captureValue)) continue;
|
|
318
|
+
if (captureValue.length <= 1) continue;
|
|
319
|
+
}
|
|
320
|
+
tokens.push({
|
|
321
|
+
type,
|
|
322
|
+
value: captureValue,
|
|
323
|
+
start: captureStart,
|
|
324
|
+
end: captureStart + captureValue.length,
|
|
325
|
+
line: lineAt(code, captureStart)
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return deduplicate(tokens);
|
|
330
|
+
}
|
|
331
|
+
function scanPython(code) {
|
|
332
|
+
const tokens = [];
|
|
333
|
+
if (!code.trim()) return tokens;
|
|
334
|
+
const patterns = [
|
|
335
|
+
// Triple-quoted docstrings
|
|
336
|
+
{ re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
|
|
337
|
+
// Line comments
|
|
338
|
+
{ re: /#[^\n]*/g, type: "Comment" /* Comment */ },
|
|
339
|
+
// Import module name
|
|
340
|
+
{ re: /\bimport\s+([A-Za-z_][A-Za-z0-9_.]*)/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
341
|
+
{ re: /\bfrom\s+([A-Za-z_.][A-Za-z0-9_.]*)\s+import/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
342
|
+
// Class name
|
|
343
|
+
{ re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
344
|
+
// Function/method def
|
|
345
|
+
{ re: /\bdef\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
346
|
+
// Variable assignment (simple name = ...)
|
|
347
|
+
{ re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
348
|
+
// Double-quoted strings
|
|
349
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
350
|
+
// Single-quoted strings
|
|
351
|
+
{ re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
|
|
352
|
+
];
|
|
353
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
354
|
+
re.lastIndex = 0;
|
|
355
|
+
let m;
|
|
356
|
+
while ((m = re.exec(code)) !== null) {
|
|
357
|
+
const fullMatch = m[0];
|
|
358
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
359
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
360
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
361
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
|
|
362
|
+
if (PYTHON_KEYWORDS.has(captureValue)) continue;
|
|
363
|
+
if (captureValue.length <= 1) continue;
|
|
364
|
+
if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
|
|
365
|
+
}
|
|
366
|
+
tokens.push({
|
|
367
|
+
type,
|
|
368
|
+
value: captureValue,
|
|
369
|
+
start: captureStart,
|
|
370
|
+
end: captureStart + captureValue.length,
|
|
371
|
+
line: lineAt(code, captureStart)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return deduplicate(tokens);
|
|
376
|
+
}
|
|
377
|
+
function scanGo(code) {
|
|
378
|
+
const tokens = [];
|
|
379
|
+
if (!code.trim()) return tokens;
|
|
380
|
+
const patterns = [
|
|
381
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
382
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
383
|
+
// Import path
|
|
384
|
+
{ re: /import\s*\(\s*([\s\S]*?)\s*\)/g, type: "Import" /* Import */ },
|
|
385
|
+
{ re: /import\s+"([^"]+)"/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
386
|
+
// Type declaration
|
|
387
|
+
{ re: /\btype\s+([A-Za-z_][A-Za-z0-9_]*)\s+(?:struct|interface)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
388
|
+
// Func name
|
|
389
|
+
{ re: /\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
390
|
+
// Var/const declaration
|
|
391
|
+
{ re: /\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
392
|
+
// Short variable declaration
|
|
393
|
+
{ re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*:=/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
394
|
+
// Double-quoted strings
|
|
395
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
396
|
+
// Backtick raw strings
|
|
397
|
+
{ re: /`[^`]*`/g, type: "String" /* String */ }
|
|
398
|
+
];
|
|
399
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
400
|
+
re.lastIndex = 0;
|
|
401
|
+
let m;
|
|
402
|
+
while ((m = re.exec(code)) !== null) {
|
|
403
|
+
const fullMatch = m[0];
|
|
404
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
405
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
406
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
407
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
|
|
408
|
+
if (GO_KEYWORDS.has(captureValue)) continue;
|
|
409
|
+
if (captureValue.length <= 1) continue;
|
|
410
|
+
}
|
|
411
|
+
tokens.push({
|
|
412
|
+
type,
|
|
413
|
+
value: captureValue,
|
|
414
|
+
start: captureStart,
|
|
415
|
+
end: captureStart + captureValue.length,
|
|
416
|
+
line: lineAt(code, captureStart)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return deduplicate(tokens);
|
|
421
|
+
}
|
|
422
|
+
function scanJava(code) {
|
|
423
|
+
const tokens = [];
|
|
424
|
+
if (!code.trim()) return tokens;
|
|
425
|
+
const patterns = [
|
|
426
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
427
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
428
|
+
// Import statement
|
|
429
|
+
{ re: /\bimport\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
430
|
+
// Class/interface/enum
|
|
431
|
+
{ re: /\b(?:class|interface|enum|record)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
432
|
+
// Method declaration
|
|
433
|
+
{ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default)[\w\s<>\[\],]*\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
434
|
+
// Variable declaration
|
|
435
|
+
{ re: /\b(?:int|long|double|float|boolean|char|byte|short|String|Object|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=;,)]/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
436
|
+
// Double-quoted strings
|
|
437
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
|
|
438
|
+
];
|
|
439
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
440
|
+
re.lastIndex = 0;
|
|
441
|
+
let m;
|
|
442
|
+
while ((m = re.exec(code)) !== null) {
|
|
443
|
+
const fullMatch = m[0];
|
|
444
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
445
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
446
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
447
|
+
if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
|
|
448
|
+
if (JAVA_KEYWORDS.has(captureValue)) continue;
|
|
449
|
+
if (captureValue.length <= 1) continue;
|
|
450
|
+
if (/^(String|Object|Integer|Long|Double|Boolean|List|Map|Set|Array|Exception|Error|System|Math|Thread|Class|Enum)$/.test(captureValue)) continue;
|
|
451
|
+
}
|
|
452
|
+
tokens.push({
|
|
453
|
+
type,
|
|
454
|
+
value: captureValue,
|
|
455
|
+
start: captureStart,
|
|
456
|
+
end: captureStart + captureValue.length,
|
|
457
|
+
line: lineAt(code, captureStart)
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return deduplicate(tokens);
|
|
462
|
+
}
|
|
463
|
+
function deduplicate(tokens) {
|
|
464
|
+
tokens.sort((a, b) => a.start - b.start);
|
|
465
|
+
const seen = /* @__PURE__ */ new Set();
|
|
466
|
+
const result = [];
|
|
467
|
+
for (const token of tokens) {
|
|
468
|
+
const key = `${token.start}:${token.end}`;
|
|
469
|
+
if (!seen.has(key)) {
|
|
470
|
+
seen.add(key);
|
|
471
|
+
result.push(token);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
function scan(code, language) {
|
|
477
|
+
if (!code || !code.trim()) {
|
|
478
|
+
return { tokens: [], language };
|
|
479
|
+
}
|
|
480
|
+
const lang = language.toLowerCase();
|
|
481
|
+
let tokens;
|
|
482
|
+
switch (lang) {
|
|
483
|
+
case "typescript":
|
|
484
|
+
case "javascript":
|
|
485
|
+
case "ts":
|
|
486
|
+
case "js":
|
|
487
|
+
case "tsx":
|
|
488
|
+
case "jsx":
|
|
489
|
+
tokens = scanTypeScript(code);
|
|
490
|
+
break;
|
|
491
|
+
case "python":
|
|
492
|
+
case "py":
|
|
493
|
+
tokens = scanPython(code);
|
|
494
|
+
break;
|
|
495
|
+
case "go":
|
|
496
|
+
tokens = scanGo(code);
|
|
497
|
+
break;
|
|
498
|
+
case "java":
|
|
499
|
+
tokens = scanJava(code);
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
tokens = scanTypeScript(code);
|
|
503
|
+
}
|
|
504
|
+
return { tokens, language: lang };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/mutator.ts
|
|
508
|
+
function hash32(str) {
|
|
509
|
+
let h = 5381;
|
|
510
|
+
for (let i = 0; i < str.length; i++) {
|
|
511
|
+
h = (h << 5) + h ^ str.charCodeAt(i);
|
|
512
|
+
}
|
|
513
|
+
return h >>> 0;
|
|
514
|
+
}
|
|
515
|
+
function toAlphaNum(n, len) {
|
|
516
|
+
const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
517
|
+
let result = "";
|
|
518
|
+
let v = n;
|
|
519
|
+
for (let i = 0; i < len; i++) {
|
|
520
|
+
result = CHARS[v % CHARS.length] + result;
|
|
521
|
+
v = Math.floor(v / CHARS.length);
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
524
|
+
}
|
|
525
|
+
function deterministicId(original, seed, prefix, len = 4) {
|
|
526
|
+
const combined = `${seed}:${prefix}:${original}`;
|
|
527
|
+
const h = hash32(combined);
|
|
528
|
+
return prefix + toAlphaNum(h, len);
|
|
529
|
+
}
|
|
530
|
+
function prefixForType(type) {
|
|
531
|
+
switch (type) {
|
|
532
|
+
case "Class" /* Class */:
|
|
533
|
+
return "_cls";
|
|
534
|
+
case "Function" /* Function */:
|
|
535
|
+
case "Method" /* Method */:
|
|
536
|
+
return "_fn";
|
|
537
|
+
case "Variable" /* Variable */:
|
|
538
|
+
return "_v";
|
|
539
|
+
case "Property" /* Property */:
|
|
540
|
+
return "_prop";
|
|
541
|
+
default:
|
|
542
|
+
return "_tok";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function mutate(code, language, seed = "pretense") {
|
|
546
|
+
const startMs = performance.now();
|
|
547
|
+
const lang = language ?? detectLanguage(code);
|
|
548
|
+
if (!code || !code.trim()) {
|
|
549
|
+
const emptyMap = /* @__PURE__ */ new Map();
|
|
550
|
+
return {
|
|
551
|
+
mutatedCode: code,
|
|
552
|
+
map: emptyMap,
|
|
553
|
+
stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const { tokens } = scan(code, lang);
|
|
557
|
+
const map = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const token of tokens) {
|
|
559
|
+
if (map.has(token.value)) continue;
|
|
560
|
+
if (token.type === "Comment" /* Comment */) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (token.type === "String" /* String */) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (token.type === "Import" /* Import */) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const prefix = prefixForType(token.type);
|
|
570
|
+
const synthetic = deterministicId(token.value, seed, prefix);
|
|
571
|
+
map.set(token.value, synthetic);
|
|
572
|
+
}
|
|
573
|
+
const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
574
|
+
let mutatedCode = code;
|
|
575
|
+
for (const [original, synthetic] of entries) {
|
|
576
|
+
if (!synthetic) continue;
|
|
577
|
+
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
578
|
+
if (original.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/)) {
|
|
579
|
+
mutatedCode = mutatedCode.replace(new RegExp(`\\b${escaped}\\b`, "g"), synthetic);
|
|
580
|
+
} else {
|
|
581
|
+
mutatedCode = mutatedCode.split(original).join(synthetic);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const durationMs = Math.round((performance.now() - startMs) * 100) / 100;
|
|
585
|
+
return {
|
|
586
|
+
mutatedCode,
|
|
587
|
+
map,
|
|
588
|
+
stats: {
|
|
589
|
+
tokensScanned: tokens.length,
|
|
590
|
+
tokensMutated: map.size,
|
|
591
|
+
durationMs,
|
|
592
|
+
language: lang
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/reverser.ts
|
|
598
|
+
function reverse(mutatedCode, map) {
|
|
599
|
+
if (!mutatedCode || map.size === 0) return mutatedCode;
|
|
600
|
+
const reverseMap = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const [original, synthetic] of map.entries()) {
|
|
602
|
+
if (synthetic === "" || !synthetic) continue;
|
|
603
|
+
reverseMap.set(synthetic, original);
|
|
604
|
+
}
|
|
605
|
+
if (reverseMap.size === 0) return mutatedCode;
|
|
606
|
+
const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
607
|
+
let result = mutatedCode;
|
|
608
|
+
for (const [synthetic, original] of sortedEntries) {
|
|
609
|
+
const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
610
|
+
if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
|
|
611
|
+
result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
|
|
612
|
+
} else {
|
|
613
|
+
result = result.split(synthetic).join(original);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/secrets.ts
|
|
620
|
+
var SECRET_PATTERNS = [
|
|
621
|
+
{ name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
|
|
622
|
+
{ name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-[A-Za-z0-9]{20,}/g },
|
|
623
|
+
{ name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
624
|
+
{ name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/g },
|
|
625
|
+
{ name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
|
|
626
|
+
{ name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
|
|
627
|
+
{ name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
|
|
628
|
+
{ name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
629
|
+
{ name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
630
|
+
{ name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
|
|
631
|
+
{ name: "generic-password", category: "credential", severity: "high", defaultAction: "redact", pattern: /(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"][^\s'"]{8,}['"]/gi },
|
|
632
|
+
{ name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
|
|
633
|
+
{ name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
|
|
634
|
+
{ name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
|
|
635
|
+
{ name: "sendgrid-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
|
|
636
|
+
{ name: "twilio-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SK[a-f0-9]{32}/g },
|
|
637
|
+
// ─── Additional patterns (v0.3) ──────────────────────────────────────────────
|
|
638
|
+
{ name: "azure-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:AccountKey|SharedAccessKey)=[A-Za-z0-9+/=]{40,}/g },
|
|
639
|
+
{ name: "mailgun-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /key-[a-f0-9]{32}/g },
|
|
640
|
+
{ name: "twilio-sid", category: "secret", severity: "high", defaultAction: "block", pattern: /AC[a-f0-9]{32}/g },
|
|
641
|
+
{ name: "heroku-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /[hH]eroku.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g },
|
|
642
|
+
{ name: "shopify-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /shpat_[a-fA-F0-9]{32}/g },
|
|
643
|
+
{ name: "datadog-key", category: "secret", severity: "high", defaultAction: "block", pattern: /dd[a-z]{1,2}_[a-zA-Z0-9]{32,}/g },
|
|
644
|
+
{ name: "vercel-token", category: "secret", severity: "high", defaultAction: "block", pattern: /vc_[a-zA-Z0-9]{24,}/g },
|
|
645
|
+
{ name: "supabase-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sbp_[a-f0-9]{40}/g },
|
|
646
|
+
{ name: "linear-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /lin_api_[A-Za-z0-9]{40}/g },
|
|
647
|
+
{ name: "cloudflare-api-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /v1\.0-[a-f0-9]{24}-[a-f0-9]{146}/g },
|
|
648
|
+
{ name: "discord-bot-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}/g },
|
|
649
|
+
{ name: "grafana-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJrIjoi[A-Za-z0-9_-]{40,}/g },
|
|
650
|
+
{ name: "confluent-key", category: "secret", severity: "high", defaultAction: "block", pattern: /CONFLUENT[A-Z_]*\s*[=:]\s*['"]?[A-Za-z0-9/+=]{16,}['"]?/g },
|
|
651
|
+
{ name: "digitalocean-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /dop_v1_[a-f0-9]{64}/g },
|
|
652
|
+
{ name: "doppler-token", category: "secret", severity: "high", defaultAction: "block", pattern: /dp\.st\.[a-z0-9_-]{2,}\.[A-Za-z0-9]{40,}/g },
|
|
653
|
+
{ name: "planetscale-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /pscale_tkn_[A-Za-z0-9_-]{32,}/g },
|
|
654
|
+
{ name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
|
|
655
|
+
{ name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
|
|
656
|
+
{ name: "algolia-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /[a-f0-9]{32}/g, validate: (m) => /ALGOLIA|algolia/.test(m) },
|
|
657
|
+
{ name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g }
|
|
658
|
+
];
|
|
659
|
+
var PII_PATTERNS = [
|
|
660
|
+
{ name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
661
|
+
{ name: "credit-card", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b(?:\d[ -]*?){13,19}\b/g, validate: luhnCheck },
|
|
662
|
+
{ name: "email", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
663
|
+
{ name: "phone-us", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
|
|
664
|
+
{ name: "ip-address", category: "pii", severity: "low", defaultAction: "warn", pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, validate: (ip) => !ip.startsWith("127.") && !ip.startsWith("0.") && ip !== "255.255.255.255" }
|
|
665
|
+
];
|
|
666
|
+
var ENV_PATTERNS = [
|
|
667
|
+
{ name: "env-secret", category: "credential", severity: "high", defaultAction: "block", pattern: /(?:process\.env\.|os\.environ\[|ENV\[|getenv\()['"]?((?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE|AUTH)[A-Z_0-9]*)['"]?\]?\)?/gi }
|
|
668
|
+
];
|
|
669
|
+
function luhnCheck(num) {
|
|
670
|
+
const digits = num.replace(/\D/g, "");
|
|
671
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
672
|
+
let sum = 0;
|
|
673
|
+
let alternate = false;
|
|
674
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
675
|
+
let n = parseInt(digits[i], 10);
|
|
676
|
+
if (alternate) {
|
|
677
|
+
n *= 2;
|
|
678
|
+
if (n > 9) n -= 9;
|
|
679
|
+
}
|
|
680
|
+
sum += n;
|
|
681
|
+
alternate = !alternate;
|
|
682
|
+
}
|
|
683
|
+
return sum % 10 === 0;
|
|
684
|
+
}
|
|
685
|
+
function shannonEntropy(str) {
|
|
686
|
+
if (str.length === 0) return 0;
|
|
687
|
+
const freq = /* @__PURE__ */ new Map();
|
|
688
|
+
for (const ch of str) {
|
|
689
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
690
|
+
}
|
|
691
|
+
let entropy = 0;
|
|
692
|
+
const len = str.length;
|
|
693
|
+
for (const count of freq.values()) {
|
|
694
|
+
const p = count / len;
|
|
695
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
696
|
+
}
|
|
697
|
+
return entropy;
|
|
698
|
+
}
|
|
699
|
+
function isHighEntropy(str, minLength = 20, threshold = 4.5) {
|
|
700
|
+
if (str.length <= minLength) return false;
|
|
701
|
+
return shannonEntropy(str) > threshold;
|
|
702
|
+
}
|
|
703
|
+
function maskValue(value, type) {
|
|
704
|
+
if (type.includes("key") || type.includes("token") || type.includes("secret")) return value.slice(0, 6) + "..." + value.slice(-4);
|
|
705
|
+
if (type === "ssn") return "***-**-" + value.slice(-4);
|
|
706
|
+
if (type === "credit-card") return "****-****-****-" + value.replace(/\D/g, "").slice(-4);
|
|
707
|
+
if (type === "email") {
|
|
708
|
+
const [local, domain] = value.split("@");
|
|
709
|
+
return (local?.[0] ?? "*") + "***@" + (domain ?? "");
|
|
710
|
+
}
|
|
711
|
+
if (value.length > 12) return value.slice(0, 4) + "..." + value.slice(-4);
|
|
712
|
+
return "***";
|
|
713
|
+
}
|
|
714
|
+
var matchCounter = 0;
|
|
715
|
+
function scan2(text, actionOverrides) {
|
|
716
|
+
const start = performance.now();
|
|
717
|
+
const matches = [];
|
|
718
|
+
const allPatterns = [...SECRET_PATTERNS, ...PII_PATTERNS, ...ENV_PATTERNS];
|
|
719
|
+
for (const def of allPatterns) {
|
|
720
|
+
def.pattern.lastIndex = 0;
|
|
721
|
+
let m;
|
|
722
|
+
while ((m = def.pattern.exec(text)) !== null) {
|
|
723
|
+
const value = m[0];
|
|
724
|
+
if (def.validate && !def.validate(value)) continue;
|
|
725
|
+
const action = actionOverrides?.[def.name] ?? def.defaultAction;
|
|
726
|
+
matchCounter++;
|
|
727
|
+
matches.push({
|
|
728
|
+
id: `scan_${matchCounter}`,
|
|
729
|
+
category: def.category,
|
|
730
|
+
type: def.name,
|
|
731
|
+
value,
|
|
732
|
+
masked: maskValue(value, def.name),
|
|
733
|
+
start: m.index,
|
|
734
|
+
end: m.index + value.length,
|
|
735
|
+
severity: def.severity,
|
|
736
|
+
action
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
|
|
741
|
+
highEntropyPattern.lastIndex = 0;
|
|
742
|
+
let hem;
|
|
743
|
+
while ((hem = highEntropyPattern.exec(text)) !== null) {
|
|
744
|
+
const value = hem[0];
|
|
745
|
+
const alreadyMatched = matches.some(
|
|
746
|
+
(m) => m.start <= hem.index && m.end >= hem.index + value.length
|
|
747
|
+
);
|
|
748
|
+
if (alreadyMatched) continue;
|
|
749
|
+
if (isHighEntropy(value)) {
|
|
750
|
+
matchCounter++;
|
|
751
|
+
matches.push({
|
|
752
|
+
id: `scan_${matchCounter}`,
|
|
753
|
+
category: "secret",
|
|
754
|
+
type: "high-entropy-string",
|
|
755
|
+
value,
|
|
756
|
+
masked: value.slice(0, 6) + "..." + value.slice(-4),
|
|
757
|
+
start: hem.index,
|
|
758
|
+
end: hem.index + value.length,
|
|
759
|
+
severity: "medium",
|
|
760
|
+
action: actionOverrides?.["high-entropy-string"] ?? "warn"
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
765
|
+
matches.sort((a, b) => a.start - b.start || severityRank[b.severity] - severityRank[a.severity]);
|
|
766
|
+
const deduped = [];
|
|
767
|
+
let lastEnd = -1;
|
|
768
|
+
for (const match of matches) {
|
|
769
|
+
if (match.start >= lastEnd) {
|
|
770
|
+
deduped.push(match);
|
|
771
|
+
lastEnd = match.end;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
clean: deduped.filter((m) => m.action === "block" || m.action === "redact").length === 0,
|
|
776
|
+
matches: deduped,
|
|
777
|
+
scannedAt: Date.now(),
|
|
778
|
+
durationMs: Math.round((performance.now() - start) * 100) / 100
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function applyRedactions(text, matches) {
|
|
782
|
+
const sorted = [...matches].sort((a, b) => b.start - a.start);
|
|
783
|
+
let result = text;
|
|
784
|
+
for (const match of sorted) {
|
|
785
|
+
if (match.action === "block" || match.action === "redact") {
|
|
786
|
+
result = result.slice(0, match.start) + `[PRETENSE:${match.type.toUpperCase()}]` + result.slice(match.end);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/store.ts
|
|
793
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
794
|
+
import { dirname } from "path";
|
|
795
|
+
var MutationStore = class {
|
|
796
|
+
entries = /* @__PURE__ */ new Map();
|
|
797
|
+
filePath;
|
|
798
|
+
constructor(filePath) {
|
|
799
|
+
this.filePath = filePath;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Persist a mutation map with associated metadata.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* store.save({ id: "req-1", originalHash: "abc", mapEntries: [...], timestamp: Date.now(), language: "typescript" });
|
|
806
|
+
*/
|
|
807
|
+
save(entry) {
|
|
808
|
+
this.entries.set(entry.id, { ...entry });
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Retrieve a stored entry by request ID.
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* const entry = store.get("req-1");
|
|
815
|
+
*/
|
|
816
|
+
get(id) {
|
|
817
|
+
return this.entries.get(id) ?? null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Find the most recent entry whose originalHash matches.
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* const entry = store.getByHash("sha256-abc123");
|
|
824
|
+
*/
|
|
825
|
+
getByHash(hash) {
|
|
826
|
+
for (const entry of [...this.entries.values()].reverse()) {
|
|
827
|
+
if (entry.originalHash === hash) return entry;
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Return all stored entries, newest first, up to `limit`.
|
|
833
|
+
*
|
|
834
|
+
* @example
|
|
835
|
+
* store.list(10) // last 10 entries
|
|
836
|
+
*/
|
|
837
|
+
list(limit = 50) {
|
|
838
|
+
const all = [...this.entries.values()].reverse();
|
|
839
|
+
return all.slice(0, limit);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Delete an entry by ID. Returns true if it existed.
|
|
843
|
+
*/
|
|
844
|
+
delete(id) {
|
|
845
|
+
return this.entries.delete(id);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Remove all entries from memory.
|
|
849
|
+
*/
|
|
850
|
+
clear() {
|
|
851
|
+
this.entries.clear();
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Write current in-memory state to the JSON file.
|
|
855
|
+
*/
|
|
856
|
+
persist() {
|
|
857
|
+
const dir = dirname(this.filePath);
|
|
858
|
+
if (!existsSync(dir)) {
|
|
859
|
+
mkdirSync(dir, { recursive: true });
|
|
860
|
+
}
|
|
861
|
+
const data = JSON.stringify([...this.entries.values()], null, 2);
|
|
862
|
+
writeFileSync(this.filePath, data, "utf-8");
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Load entries from the JSON file into memory.
|
|
866
|
+
* No-ops if the file doesn't exist.
|
|
867
|
+
*/
|
|
868
|
+
load() {
|
|
869
|
+
if (!existsSync(this.filePath)) return;
|
|
870
|
+
try {
|
|
871
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
872
|
+
const parsed = JSON.parse(raw);
|
|
873
|
+
this.entries.clear();
|
|
874
|
+
for (const entry of parsed) {
|
|
875
|
+
this.entries.set(entry.id, entry);
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
this.entries.clear();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/** Total entries in memory */
|
|
882
|
+
get size() {
|
|
883
|
+
return this.entries.size;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Reconstruct a MutationMap from a stored entry.
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* const map = store.toMap(entry);
|
|
890
|
+
* reverse(mutatedCode, map);
|
|
891
|
+
*/
|
|
892
|
+
static toMap(entry) {
|
|
893
|
+
return new Map(entry.mapEntries);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Serialize a MutationMap to the format stored in StoreEntry.
|
|
897
|
+
*/
|
|
898
|
+
static fromMap(map) {
|
|
899
|
+
return [...map.entries()];
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/config.ts
|
|
904
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
905
|
+
import { join, resolve } from "path";
|
|
906
|
+
var CONFIG_DIR_NAME = ".pretense";
|
|
907
|
+
var CONFIG_FILE = "config.json";
|
|
908
|
+
var MUTATION_MAP_FILE = "mutation-map.json";
|
|
909
|
+
var AUDIT_LOG_FILE = "audit.log";
|
|
910
|
+
var USAGE_FILE = "usage.json";
|
|
911
|
+
function getConfigDir(baseDir) {
|
|
912
|
+
const base = baseDir ?? process.cwd();
|
|
913
|
+
return resolve(base, CONFIG_DIR_NAME);
|
|
914
|
+
}
|
|
915
|
+
function initConfig(dir) {
|
|
916
|
+
const configDir = getConfigDir(dir);
|
|
917
|
+
if (!existsSync2(configDir)) {
|
|
918
|
+
mkdirSync2(configDir, { recursive: true });
|
|
919
|
+
}
|
|
920
|
+
const configPath = join(configDir, CONFIG_FILE);
|
|
921
|
+
if (!existsSync2(configPath)) {
|
|
922
|
+
const config = {
|
|
923
|
+
...DEFAULT_CONFIG,
|
|
924
|
+
storePath: configDir
|
|
925
|
+
};
|
|
926
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
927
|
+
}
|
|
928
|
+
const mapPath = join(configDir, MUTATION_MAP_FILE);
|
|
929
|
+
if (!existsSync2(mapPath)) {
|
|
930
|
+
writeFileSync2(mapPath, "[]", "utf-8");
|
|
931
|
+
}
|
|
932
|
+
const auditPath = join(configDir, AUDIT_LOG_FILE);
|
|
933
|
+
if (!existsSync2(auditPath)) {
|
|
934
|
+
writeFileSync2(auditPath, "", "utf-8");
|
|
935
|
+
}
|
|
936
|
+
const usagePath = join(configDir, USAGE_FILE);
|
|
937
|
+
if (!existsSync2(usagePath)) {
|
|
938
|
+
writeFileSync2(
|
|
939
|
+
usagePath,
|
|
940
|
+
JSON.stringify({ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), mutations: 0, firstUseDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }, null, 2),
|
|
941
|
+
"utf-8"
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return configDir;
|
|
945
|
+
}
|
|
946
|
+
function loadConfig(dir) {
|
|
947
|
+
const configDir = getConfigDir(dir);
|
|
948
|
+
const configPath = join(configDir, CONFIG_FILE);
|
|
949
|
+
if (!existsSync2(configPath)) {
|
|
950
|
+
return { ...DEFAULT_CONFIG };
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
954
|
+
const parsed = JSON.parse(raw);
|
|
955
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
956
|
+
} catch {
|
|
957
|
+
return { ...DEFAULT_CONFIG };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function loadUsage(dir) {
|
|
961
|
+
const configDir = getConfigDir(dir);
|
|
962
|
+
const usagePath = join(configDir, USAGE_FILE);
|
|
963
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
964
|
+
if (!existsSync2(usagePath)) {
|
|
965
|
+
return { date: today, mutations: 0, firstUseDate: today };
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const raw = readFileSync2(usagePath, "utf-8");
|
|
969
|
+
const data = JSON.parse(raw);
|
|
970
|
+
if (data.date !== today) {
|
|
971
|
+
return { date: today, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
972
|
+
}
|
|
973
|
+
return data;
|
|
974
|
+
} catch {
|
|
975
|
+
return { date: today, mutations: 0, firstUseDate: today };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function saveUsage(usage, dir) {
|
|
979
|
+
const configDir = getConfigDir(dir);
|
|
980
|
+
if (!existsSync2(configDir)) {
|
|
981
|
+
mkdirSync2(configDir, { recursive: true });
|
|
982
|
+
}
|
|
983
|
+
const usagePath = join(configDir, USAGE_FILE);
|
|
984
|
+
writeFileSync2(usagePath, JSON.stringify(usage, null, 2), "utf-8");
|
|
985
|
+
}
|
|
986
|
+
function detectTier() {
|
|
987
|
+
const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
|
|
988
|
+
if (key.startsWith("pre_ent_")) return "enterprise";
|
|
989
|
+
if (key.startsWith("pre_pro_")) return "pro";
|
|
990
|
+
return "free";
|
|
991
|
+
}
|
|
992
|
+
function getTierLimits(tier) {
|
|
993
|
+
switch (tier) {
|
|
994
|
+
case "enterprise":
|
|
995
|
+
return { dailyMutations: Infinity, auditRetentionDays: Infinity };
|
|
996
|
+
case "pro":
|
|
997
|
+
return { dailyMutations: Infinity, auditRetentionDays: 90 };
|
|
998
|
+
case "free":
|
|
999
|
+
default:
|
|
1000
|
+
return { dailyMutations: 500, auditRetentionDays: 3 };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/audit.ts
|
|
1005
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1006
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1007
|
+
function writeAuditEntry(entry, dir) {
|
|
1008
|
+
const configDir = getConfigDir(dir);
|
|
1009
|
+
const auditPath = join2(configDir, "audit.log");
|
|
1010
|
+
const logDir = dirname2(auditPath);
|
|
1011
|
+
if (!existsSync3(logDir)) {
|
|
1012
|
+
mkdirSync3(logDir, { recursive: true });
|
|
1013
|
+
}
|
|
1014
|
+
const line = JSON.stringify(entry) + "\n";
|
|
1015
|
+
appendFileSync(auditPath, line, "utf-8");
|
|
1016
|
+
}
|
|
1017
|
+
function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, llmProvider, latencyMs) {
|
|
1018
|
+
return {
|
|
1019
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1020
|
+
sessionId,
|
|
1021
|
+
file,
|
|
1022
|
+
identifiersMutated,
|
|
1023
|
+
secretsBlocked,
|
|
1024
|
+
llmProvider,
|
|
1025
|
+
latencyMs
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function readAuditLog(options = {}) {
|
|
1029
|
+
const { limit, dir } = options;
|
|
1030
|
+
const configDir = getConfigDir(dir);
|
|
1031
|
+
const auditPath = join2(configDir, "audit.log");
|
|
1032
|
+
if (!existsSync3(auditPath)) {
|
|
1033
|
+
return [];
|
|
1034
|
+
}
|
|
1035
|
+
const raw = readFileSync3(auditPath, "utf-8");
|
|
1036
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
1037
|
+
let entries = [];
|
|
1038
|
+
for (const line of lines) {
|
|
1039
|
+
try {
|
|
1040
|
+
entries.push(JSON.parse(line));
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const tier = detectTier();
|
|
1045
|
+
const { auditRetentionDays } = getTierLimits(tier);
|
|
1046
|
+
if (auditRetentionDays !== Infinity) {
|
|
1047
|
+
const cutoff = Date.now() - auditRetentionDays * 24 * 60 * 60 * 1e3;
|
|
1048
|
+
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
|
|
1049
|
+
}
|
|
1050
|
+
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1051
|
+
if (limit && limit > 0) {
|
|
1052
|
+
entries = entries.slice(0, limit);
|
|
1053
|
+
}
|
|
1054
|
+
return entries;
|
|
1055
|
+
}
|
|
1056
|
+
function formatJson(entries) {
|
|
1057
|
+
return JSON.stringify(entries, null, 2);
|
|
1058
|
+
}
|
|
1059
|
+
function formatCsv(entries) {
|
|
1060
|
+
const headers = "timestamp,sessionId,file,identifiersMutated,secretsBlocked,llmProvider,latencyMs";
|
|
1061
|
+
const rows = entries.map(
|
|
1062
|
+
(e) => [
|
|
1063
|
+
e.timestamp,
|
|
1064
|
+
e.sessionId,
|
|
1065
|
+
`"${e.file.replace(/"/g, '""')}"`,
|
|
1066
|
+
e.identifiersMutated,
|
|
1067
|
+
e.secretsBlocked,
|
|
1068
|
+
e.llmProvider,
|
|
1069
|
+
e.latencyMs
|
|
1070
|
+
].join(",")
|
|
1071
|
+
);
|
|
1072
|
+
return [headers, ...rows].join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
function formatText(entries) {
|
|
1075
|
+
if (entries.length === 0) return "No audit entries found.";
|
|
1076
|
+
const lines = entries.map((e) => {
|
|
1077
|
+
const date = new Date(e.timestamp).toLocaleString();
|
|
1078
|
+
return `[${date}] ${e.file} | ${e.identifiersMutated} mutated | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
|
|
1079
|
+
});
|
|
1080
|
+
return lines.join("\n");
|
|
1081
|
+
}
|
|
1082
|
+
function formatAudit(entries, format = "text") {
|
|
1083
|
+
switch (format) {
|
|
1084
|
+
case "json":
|
|
1085
|
+
return formatJson(entries);
|
|
1086
|
+
case "csv":
|
|
1087
|
+
return formatCsv(entries);
|
|
1088
|
+
case "text":
|
|
1089
|
+
default:
|
|
1090
|
+
return formatText(entries);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/proxy.ts
|
|
1095
|
+
import { Hono } from "hono";
|
|
1096
|
+
import { serve } from "@hono/node-server";
|
|
1097
|
+
import { nanoid } from "nanoid";
|
|
1098
|
+
function detectUpstream(path) {
|
|
1099
|
+
if (path.includes("/v1/messages")) return "https://api.anthropic.com";
|
|
1100
|
+
if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "https://api.openai.com";
|
|
1101
|
+
if (path.includes("/v1beta/") || path.includes("/v1/models")) return "https://generativelanguage.googleapis.com";
|
|
1102
|
+
return "https://api.openai.com";
|
|
1103
|
+
}
|
|
1104
|
+
function detectProvider(path) {
|
|
1105
|
+
if (path.includes("/v1/messages")) return "anthropic";
|
|
1106
|
+
if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "openai";
|
|
1107
|
+
if (path.includes("/v1beta/") || path.includes("/v1/models")) return "google";
|
|
1108
|
+
return "openai";
|
|
1109
|
+
}
|
|
1110
|
+
function extractText(body) {
|
|
1111
|
+
const parts = [];
|
|
1112
|
+
if (typeof body["system"] === "string") parts.push(body["system"]);
|
|
1113
|
+
if (Array.isArray(body["messages"])) {
|
|
1114
|
+
for (const msg of body["messages"]) {
|
|
1115
|
+
if (typeof msg["content"] === "string") parts.push(msg["content"]);
|
|
1116
|
+
else if (Array.isArray(msg["content"])) {
|
|
1117
|
+
for (const block of msg["content"]) {
|
|
1118
|
+
if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return parts.join("\n");
|
|
1124
|
+
}
|
|
1125
|
+
function extractCodeBlocks(text) {
|
|
1126
|
+
const blocks = [];
|
|
1127
|
+
const re = /```(\w*)\n([\s\S]*?)```/g;
|
|
1128
|
+
let m;
|
|
1129
|
+
while ((m = re.exec(text)) !== null) {
|
|
1130
|
+
blocks.push({
|
|
1131
|
+
code: m[2] ?? "",
|
|
1132
|
+
language: m[1] ?? "typescript",
|
|
1133
|
+
start: m.index,
|
|
1134
|
+
end: m.index + m[0].length
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return blocks;
|
|
1138
|
+
}
|
|
1139
|
+
function replaceCodeBlocks(text, blocks, replacements) {
|
|
1140
|
+
let result = text;
|
|
1141
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
1142
|
+
const block = blocks[i];
|
|
1143
|
+
const replacement = replacements[i] ?? block.code;
|
|
1144
|
+
const lang = block.language ?? "typescript";
|
|
1145
|
+
result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
function applyToBody(body, transform) {
|
|
1150
|
+
const mutateField = (val) => {
|
|
1151
|
+
if (typeof val === "string") return transform(val);
|
|
1152
|
+
if (Array.isArray(val)) return val.map(mutateField);
|
|
1153
|
+
if (val && typeof val === "object") {
|
|
1154
|
+
const obj = val;
|
|
1155
|
+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
|
|
1156
|
+
}
|
|
1157
|
+
return val;
|
|
1158
|
+
};
|
|
1159
|
+
return mutateField(body);
|
|
1160
|
+
}
|
|
1161
|
+
var stats = {
|
|
1162
|
+
requestsProcessed: 0,
|
|
1163
|
+
totalTokensMutated: 0,
|
|
1164
|
+
totalSecretsBlocked: 0,
|
|
1165
|
+
startedAt: Date.now()
|
|
1166
|
+
};
|
|
1167
|
+
function daysSinceFirstUse() {
|
|
1168
|
+
const usage = loadUsage();
|
|
1169
|
+
if (!usage.firstUseDate) return 0;
|
|
1170
|
+
const first = new Date(usage.firstUseDate).getTime();
|
|
1171
|
+
return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
|
|
1172
|
+
}
|
|
1173
|
+
function printUpgradeNudge(reason) {
|
|
1174
|
+
process.stdout.write(
|
|
1175
|
+
`
|
|
1176
|
+
\x1B[33m[PRETENSE] ${reason}\x1B[0m
|
|
1177
|
+
\x1B[33m[PRETENSE] Upgrade to Pro: https://pretense.ai/pricing\x1B[0m
|
|
1178
|
+
|
|
1179
|
+
`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
function createProxy(opts = {}) {
|
|
1183
|
+
const config = { ...DEFAULT_CONFIG, ...opts.config };
|
|
1184
|
+
const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
|
|
1185
|
+
const verbose = opts.verbose ?? false;
|
|
1186
|
+
const app = new Hono();
|
|
1187
|
+
app.get(
|
|
1188
|
+
"/health",
|
|
1189
|
+
(c) => c.json({
|
|
1190
|
+
status: "ok",
|
|
1191
|
+
version: "0.3.0",
|
|
1192
|
+
uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
|
|
1193
|
+
stats: {
|
|
1194
|
+
requestsProcessed: stats.requestsProcessed,
|
|
1195
|
+
totalTokensMutated: stats.totalTokensMutated
|
|
1196
|
+
}
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
app.get(
|
|
1200
|
+
"/stats",
|
|
1201
|
+
(c) => c.json({
|
|
1202
|
+
...stats,
|
|
1203
|
+
storeSize: store.size
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
app.get("/audit", (c) => {
|
|
1207
|
+
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
1208
|
+
return c.json(store.list(limit));
|
|
1209
|
+
});
|
|
1210
|
+
app.all("/*", async (c) => {
|
|
1211
|
+
if (c.req.method !== "POST") {
|
|
1212
|
+
const upstream2 = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1213
|
+
const url = upstream2 + c.req.path;
|
|
1214
|
+
const headers2 = {};
|
|
1215
|
+
c.req.raw.headers.forEach((v, k) => {
|
|
1216
|
+
headers2[k] = v;
|
|
1217
|
+
});
|
|
1218
|
+
delete headers2["host"];
|
|
1219
|
+
headers2["host"] = new URL(upstream2).host;
|
|
1220
|
+
const resp = await fetch(url, { method: c.req.method, headers: headers2 });
|
|
1221
|
+
return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
|
|
1222
|
+
}
|
|
1223
|
+
const requestStart = performance.now();
|
|
1224
|
+
let body;
|
|
1225
|
+
try {
|
|
1226
|
+
body = await c.req.json();
|
|
1227
|
+
} catch {
|
|
1228
|
+
return c.json({ error: { type: "invalid_request", message: "Invalid JSON body" } }, 400);
|
|
1229
|
+
}
|
|
1230
|
+
if (!body || Object.keys(body).length === 0) {
|
|
1231
|
+
return c.json({ error: { type: "invalid_request", message: "Empty request body" } }, 400);
|
|
1232
|
+
}
|
|
1233
|
+
const requestId = nanoid(12);
|
|
1234
|
+
const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1235
|
+
const provider = detectProvider(c.req.path);
|
|
1236
|
+
const tier = detectTier();
|
|
1237
|
+
const { dailyMutations } = getTierLimits(tier);
|
|
1238
|
+
const usage = loadUsage();
|
|
1239
|
+
if (tier === "free" && usage.mutations >= dailyMutations) {
|
|
1240
|
+
return c.json(
|
|
1241
|
+
{
|
|
1242
|
+
error: {
|
|
1243
|
+
type: "pretense_rate_limit",
|
|
1244
|
+
message: `Daily mutation limit reached (${dailyMutations}/day). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
429
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
const fullText = extractText(body);
|
|
1251
|
+
const secretScan = scan2(fullText);
|
|
1252
|
+
const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
|
|
1253
|
+
if (blockedSecrets.length > 0) {
|
|
1254
|
+
stats.totalSecretsBlocked += blockedSecrets.length;
|
|
1255
|
+
if (tier === "free") {
|
|
1256
|
+
printUpgradeNudge(`${blockedSecrets.length} secret(s) blocked. Pro tier includes real-time secret alerts via Slack/PagerDuty.`);
|
|
1257
|
+
}
|
|
1258
|
+
return c.json(
|
|
1259
|
+
{
|
|
1260
|
+
error: {
|
|
1261
|
+
type: "pretense_blocked",
|
|
1262
|
+
message: `Request blocked: ${blockedSecrets.length} secret(s) detected. Types: ${[...new Set(blockedSecrets.map((m) => m.type))].join(", ")}`
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
400
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const processedBody = applyToBody(body, (text) => applyRedactions(text, secretScan.matches));
|
|
1269
|
+
const mutationMap = /* @__PURE__ */ new Map();
|
|
1270
|
+
let tokensMutated = 0;
|
|
1271
|
+
const mutatedBody = applyToBody(processedBody, (text) => {
|
|
1272
|
+
const blocks = extractCodeBlocks(text);
|
|
1273
|
+
if (blocks.length === 0) return text;
|
|
1274
|
+
const mutatedBlocks = [];
|
|
1275
|
+
for (const block of blocks) {
|
|
1276
|
+
const result = mutate(block.code, block.language);
|
|
1277
|
+
for (const [k, v] of result.map.entries()) mutationMap.set(k, v);
|
|
1278
|
+
tokensMutated += result.stats.tokensMutated;
|
|
1279
|
+
mutatedBlocks.push(result.mutatedCode);
|
|
1280
|
+
}
|
|
1281
|
+
return replaceCodeBlocks(text, blocks, mutatedBlocks);
|
|
1282
|
+
});
|
|
1283
|
+
usage.mutations += tokensMutated;
|
|
1284
|
+
saveUsage(usage);
|
|
1285
|
+
if (tier === "free" && usage.mutations >= 400 && usage.mutations - tokensMutated < 400) {
|
|
1286
|
+
printUpgradeNudge(`${usage.mutations}/${dailyMutations} daily mutations used. Running low!`);
|
|
1287
|
+
}
|
|
1288
|
+
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
1289
|
+
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
|
|
1290
|
+
}
|
|
1291
|
+
const entry = {
|
|
1292
|
+
id: requestId,
|
|
1293
|
+
originalHash: requestId,
|
|
1294
|
+
mapEntries: [...mutationMap.entries()],
|
|
1295
|
+
timestamp: Date.now(),
|
|
1296
|
+
language: "mixed"
|
|
1297
|
+
};
|
|
1298
|
+
store.save(entry);
|
|
1299
|
+
stats.requestsProcessed++;
|
|
1300
|
+
stats.totalTokensMutated += tokensMutated;
|
|
1301
|
+
const latencyMs = Math.round((performance.now() - requestStart) * 100) / 100;
|
|
1302
|
+
writeAuditEntry(
|
|
1303
|
+
createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
|
|
1304
|
+
);
|
|
1305
|
+
if (verbose && tokensMutated > 0) {
|
|
1306
|
+
process.stdout.write(`[PRETENSE] ${tokensMutated} tokens mutated for request ${requestId}
|
|
1307
|
+
`);
|
|
1308
|
+
}
|
|
1309
|
+
const headers = {};
|
|
1310
|
+
c.req.raw.headers.forEach((v, k) => {
|
|
1311
|
+
headers[k] = v;
|
|
1312
|
+
});
|
|
1313
|
+
delete headers["host"];
|
|
1314
|
+
delete headers["content-length"];
|
|
1315
|
+
headers["host"] = new URL(upstream).host;
|
|
1316
|
+
headers["x-pretense-request-id"] = requestId;
|
|
1317
|
+
const forwardBody = JSON.stringify(mutatedBody);
|
|
1318
|
+
let upstreamResp;
|
|
1319
|
+
try {
|
|
1320
|
+
upstreamResp = await fetch(upstream + c.req.path, {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
headers: { ...headers, "content-type": "application/json", "content-length": String(Buffer.byteLength(forwardBody)) },
|
|
1323
|
+
body: forwardBody
|
|
1324
|
+
});
|
|
1325
|
+
} catch {
|
|
1326
|
+
return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
|
|
1327
|
+
}
|
|
1328
|
+
if (body["stream"] === true) {
|
|
1329
|
+
return new Response(upstreamResp.body, {
|
|
1330
|
+
status: upstreamResp.status,
|
|
1331
|
+
headers: {
|
|
1332
|
+
"content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
|
|
1333
|
+
"cache-control": "no-cache",
|
|
1334
|
+
"x-pretense-request-id": requestId,
|
|
1335
|
+
"x-pretense-protected": "true",
|
|
1336
|
+
"x-pretense-mutations": String(tokensMutated)
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const reverseMap = new Map(mutationMap);
|
|
1341
|
+
let respBody;
|
|
1342
|
+
try {
|
|
1343
|
+
respBody = await upstreamResp.json();
|
|
1344
|
+
} catch {
|
|
1345
|
+
return new Response(null, {
|
|
1346
|
+
status: upstreamResp.status,
|
|
1347
|
+
headers: {
|
|
1348
|
+
"x-pretense-protected": "true",
|
|
1349
|
+
"x-pretense-mutations": String(tokensMutated)
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
const reversedBody = applyToBody(respBody, (text) => {
|
|
1354
|
+
const blocks = extractCodeBlocks(text);
|
|
1355
|
+
if (blocks.length === 0) return reverse(text, reverseMap);
|
|
1356
|
+
const reversedBlocks = blocks.map((b) => reverse(b.code, reverseMap));
|
|
1357
|
+
return replaceCodeBlocks(text, blocks, reversedBlocks);
|
|
1358
|
+
});
|
|
1359
|
+
c.header("x-pretense-request-id", requestId);
|
|
1360
|
+
c.header("x-pretense-protected", "true");
|
|
1361
|
+
c.header("x-pretense-mutations", String(tokensMutated));
|
|
1362
|
+
return c.json(reversedBody, upstreamResp.status);
|
|
1363
|
+
});
|
|
1364
|
+
return app;
|
|
1365
|
+
}
|
|
1366
|
+
function startProxy(opts = {}) {
|
|
1367
|
+
const port = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
1368
|
+
const app = createProxy(opts);
|
|
1369
|
+
const tier = detectTier();
|
|
1370
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
1371
|
+
process.stdout.write(`
|
|
1372
|
+
\x1B[36m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
1373
|
+
\u2502 \u2502
|
|
1374
|
+
\u2502 \x1B[1mPretense\x1B[0m\x1B[36m v0.3.0 [${tier.toUpperCase()}]${" ".repeat(20 - tier.length)}\u2502
|
|
1375
|
+
\u2502 Your code hits AI naked. We fix that. \u2502
|
|
1376
|
+
\u2502 \u2502
|
|
1377
|
+
\u2502 Proxy: http://localhost:${port} \u2502
|
|
1378
|
+
\u2502 Health: http://localhost:${port}/health \u2502
|
|
1379
|
+
\u2502 Stats: http://localhost:${port}/stats \u2502
|
|
1380
|
+
\u2502 \u2502
|
|
1381
|
+
\u2502 ANTHROPIC_BASE_URL=http://localhost:${port} \u2502
|
|
1382
|
+
\u2502 OPENAI_BASE_URL=http://localhost:${port}/v1 \u2502
|
|
1383
|
+
\u2502 \u2502
|
|
1384
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m
|
|
1385
|
+
`);
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/index.ts
|
|
1390
|
+
var VERSION = "0.3.0";
|
|
1391
|
+
function langFromExt(filePath) {
|
|
1392
|
+
const ext = extname(filePath).toLowerCase();
|
|
1393
|
+
const map = {
|
|
1394
|
+
".ts": "typescript",
|
|
1395
|
+
".tsx": "typescript",
|
|
1396
|
+
".js": "javascript",
|
|
1397
|
+
".jsx": "javascript",
|
|
1398
|
+
".mjs": "javascript",
|
|
1399
|
+
".cjs": "javascript",
|
|
1400
|
+
".py": "python",
|
|
1401
|
+
".go": "go",
|
|
1402
|
+
".java": "java"
|
|
1403
|
+
};
|
|
1404
|
+
return map[ext] ?? "typescript";
|
|
1405
|
+
}
|
|
1406
|
+
function collectFiles(target) {
|
|
1407
|
+
const abs = resolve2(target);
|
|
1408
|
+
if (!existsSync4(abs)) {
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
const stat = statSync(abs);
|
|
1412
|
+
if (stat.isFile()) return [abs];
|
|
1413
|
+
const files = [];
|
|
1414
|
+
const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
|
|
1415
|
+
function walk(dir) {
|
|
1416
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1417
|
+
const fullPath = join3(dir, entry.name);
|
|
1418
|
+
if (entry.isDirectory()) {
|
|
1419
|
+
if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
|
|
1420
|
+
walk(fullPath);
|
|
1421
|
+
} else if (entry.isFile() && supportedExts.has(extname(entry.name).toLowerCase())) {
|
|
1422
|
+
files.push(fullPath);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
walk(abs);
|
|
1427
|
+
return files;
|
|
1428
|
+
}
|
|
1429
|
+
var program = new Command();
|
|
1430
|
+
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION);
|
|
1431
|
+
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
1432
|
+
const dir = resolve2(opts.dir);
|
|
1433
|
+
console.log(chalk.cyan("Initializing Pretense in"), chalk.bold(dir));
|
|
1434
|
+
const configDir = initConfig(dir);
|
|
1435
|
+
const files = collectFiles(dir);
|
|
1436
|
+
const langCounts = {};
|
|
1437
|
+
for (const f of files) {
|
|
1438
|
+
const lang = langFromExt(f);
|
|
1439
|
+
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
1440
|
+
}
|
|
1441
|
+
console.log(chalk.green("\n .pretense/ created at"), chalk.bold(configDir));
|
|
1442
|
+
console.log(chalk.gray(`
|
|
1443
|
+
Scanned ${files.length} source files:`));
|
|
1444
|
+
for (const [lang, count] of Object.entries(langCounts)) {
|
|
1445
|
+
console.log(chalk.gray(` ${lang}: ${count} files`));
|
|
1446
|
+
}
|
|
1447
|
+
console.log(chalk.cyan("\n Next: run"), chalk.bold("pretense start"), chalk.cyan("to launch the proxy"));
|
|
1448
|
+
console.log();
|
|
1449
|
+
});
|
|
1450
|
+
program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action((opts) => {
|
|
1451
|
+
const config = loadConfig();
|
|
1452
|
+
const port = parseInt(opts.port) || config.port;
|
|
1453
|
+
startProxy({ config, port, verbose: opts.verbose });
|
|
1454
|
+
});
|
|
1455
|
+
program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).action((target, opts) => {
|
|
1456
|
+
const files = collectFiles(target);
|
|
1457
|
+
if (files.length === 0) {
|
|
1458
|
+
console.error(chalk.red("No source files found at"), chalk.bold(target));
|
|
1459
|
+
process.exit(1);
|
|
1460
|
+
}
|
|
1461
|
+
let totalIdentifiers = 0;
|
|
1462
|
+
let totalSecrets = 0;
|
|
1463
|
+
const allResults = [];
|
|
1464
|
+
for (const file of files) {
|
|
1465
|
+
const code = readFileSync4(file, "utf-8");
|
|
1466
|
+
const lang = langFromExt(file);
|
|
1467
|
+
let identifiers = 0;
|
|
1468
|
+
if (!opts.secretsOnly) {
|
|
1469
|
+
const scanResult = scan(code, lang);
|
|
1470
|
+
identifiers = scanResult.tokens.length;
|
|
1471
|
+
totalIdentifiers += identifiers;
|
|
1472
|
+
}
|
|
1473
|
+
const secretResult = scan2(code);
|
|
1474
|
+
const blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
|
|
1475
|
+
totalSecrets += blocked.length;
|
|
1476
|
+
allResults.push({
|
|
1477
|
+
file,
|
|
1478
|
+
identifiers,
|
|
1479
|
+
secrets: blocked.length,
|
|
1480
|
+
secretTypes: blocked.map((m) => m.type)
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (opts.json) {
|
|
1484
|
+
console.log(JSON.stringify({ files: allResults, totalIdentifiers, totalSecrets }, null, 2));
|
|
1485
|
+
} else {
|
|
1486
|
+
console.log(chalk.cyan("\nPretense Scan Results\n"));
|
|
1487
|
+
for (const r of allResults) {
|
|
1488
|
+
const secretBadge = r.secrets > 0 ? chalk.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
1489
|
+
console.log(
|
|
1490
|
+
chalk.gray(" ") + chalk.white(r.file) + chalk.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
1491
|
+
);
|
|
1492
|
+
if (r.secrets > 0) {
|
|
1493
|
+
for (const t of r.secretTypes) {
|
|
1494
|
+
console.log(chalk.red(` ! ${t}`));
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
console.log(chalk.gray(`
|
|
1499
|
+
Total: ${files.length} files, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk.red(`${totalSecrets} secrets found`) : chalk.green("0 secrets")));
|
|
1500
|
+
console.log();
|
|
1501
|
+
}
|
|
1502
|
+
if (totalSecrets > 0) {
|
|
1503
|
+
process.exit(2);
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
|
|
1507
|
+
const absPath = resolve2(file);
|
|
1508
|
+
if (!existsSync4(absPath)) {
|
|
1509
|
+
console.error(chalk.red("File not found:"), chalk.bold(absPath));
|
|
1510
|
+
process.exit(1);
|
|
1511
|
+
}
|
|
1512
|
+
const code = readFileSync4(absPath, "utf-8");
|
|
1513
|
+
const lang = langFromExt(absPath);
|
|
1514
|
+
const result = mutate(code, lang, opts.seed);
|
|
1515
|
+
if (opts.save) {
|
|
1516
|
+
const configDir = getConfigDir();
|
|
1517
|
+
const store = new MutationStore(join3(configDir, "mutation-map.json"));
|
|
1518
|
+
store.load();
|
|
1519
|
+
store.save({
|
|
1520
|
+
id: absPath,
|
|
1521
|
+
originalHash: absPath,
|
|
1522
|
+
mapEntries: MutationStore.fromMap(result.map),
|
|
1523
|
+
timestamp: Date.now(),
|
|
1524
|
+
language: lang
|
|
1525
|
+
});
|
|
1526
|
+
store.persist();
|
|
1527
|
+
process.stderr.write(chalk.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
1528
|
+
`));
|
|
1529
|
+
}
|
|
1530
|
+
if (opts.json) {
|
|
1531
|
+
console.log(JSON.stringify({
|
|
1532
|
+
mutatedCode: result.mutatedCode,
|
|
1533
|
+
map: Object.fromEntries(result.map),
|
|
1534
|
+
stats: result.stats
|
|
1535
|
+
}, null, 2));
|
|
1536
|
+
} else {
|
|
1537
|
+
process.stdout.write(result.mutatedCode);
|
|
1538
|
+
process.stderr.write(chalk.gray(`
|
|
1539
|
+
--- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
|
|
1540
|
+
`));
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
1544
|
+
const absPath = resolve2(file);
|
|
1545
|
+
if (!existsSync4(absPath)) {
|
|
1546
|
+
console.error(chalk.red("File not found:"), chalk.bold(absPath));
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
}
|
|
1549
|
+
const mutatedCode = readFileSync4(absPath, "utf-8");
|
|
1550
|
+
const mapPath = opts.map ? resolve2(opts.map) : join3(getConfigDir(), "mutation-map.json");
|
|
1551
|
+
if (!existsSync4(mapPath)) {
|
|
1552
|
+
console.error(chalk.red("Mutation map not found:"), chalk.bold(mapPath));
|
|
1553
|
+
console.error(chalk.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
const store = new MutationStore(mapPath);
|
|
1557
|
+
store.load();
|
|
1558
|
+
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
1559
|
+
if (!entry) {
|
|
1560
|
+
console.error(chalk.red("No mutation map found for this file."));
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
const map = MutationStore.toMap(entry);
|
|
1564
|
+
const restored = reverse(mutatedCode, map);
|
|
1565
|
+
process.stdout.write(restored);
|
|
1566
|
+
process.stderr.write(chalk.gray(`
|
|
1567
|
+
--- Reversed ${map.size} mutations ---
|
|
1568
|
+
`));
|
|
1569
|
+
});
|
|
1570
|
+
program.command("audit").description("View the audit log").option("--json", "Output as JSON", false).option("--csv", "Output as CSV", false).option("-l, --limit <number>", "Limit entries", "50").action((opts) => {
|
|
1571
|
+
const format = opts.json ? "json" : opts.csv ? "csv" : "text";
|
|
1572
|
+
const limit = parseInt(opts.limit) || 50;
|
|
1573
|
+
const entries = readAuditLog({ limit, format });
|
|
1574
|
+
const output = formatAudit(entries, format);
|
|
1575
|
+
console.log(output);
|
|
1576
|
+
});
|
|
1577
|
+
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
1578
|
+
console.log(`@pretense/cli v${VERSION}`);
|
|
1579
|
+
});
|
|
1580
|
+
program.parse();
|
|
1581
|
+
//# sourceMappingURL=index.js.map
|