@pretense/cli 0.3.0 → 0.4.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 +23 -10
- package/dist/index.js +2549 -236
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/bin/pretense +0 -2
package/dist/index.js
CHANGED
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import chalk9 from "chalk";
|
|
6
|
+
import {
|
|
7
|
+
readFileSync as readFileSync7,
|
|
8
|
+
existsSync as existsSync7,
|
|
9
|
+
readdirSync,
|
|
10
|
+
statSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readSync as readSync2,
|
|
13
|
+
closeSync
|
|
14
|
+
} from "fs";
|
|
15
|
+
import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
|
|
16
|
+
import { resolve as resolve2, extname, join as join5, basename } from "path";
|
|
17
|
+
import os from "os";
|
|
8
18
|
|
|
9
19
|
// src/types.ts
|
|
10
20
|
var DEFAULT_CONFIG = {
|
|
11
21
|
port: 9339,
|
|
12
22
|
targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
|
|
13
|
-
languages: ["typescript", "javascript", "python", "go", "java"],
|
|
23
|
+
languages: ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"],
|
|
14
24
|
mutationRules: [
|
|
15
25
|
{ tokenType: "Variable" /* Variable */, prefix: "_v" },
|
|
16
26
|
{ tokenType: "Function" /* Function */, prefix: "_fn" },
|
|
@@ -262,29 +272,200 @@ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
262
272
|
"permits",
|
|
263
273
|
"yield"
|
|
264
274
|
]);
|
|
275
|
+
var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
|
|
276
|
+
"class",
|
|
277
|
+
"interface",
|
|
278
|
+
"namespace",
|
|
279
|
+
"public",
|
|
280
|
+
"private",
|
|
281
|
+
"protected",
|
|
282
|
+
"internal",
|
|
283
|
+
"static",
|
|
284
|
+
"void",
|
|
285
|
+
"return",
|
|
286
|
+
"new",
|
|
287
|
+
"using",
|
|
288
|
+
"override",
|
|
289
|
+
"sealed",
|
|
290
|
+
"abstract",
|
|
291
|
+
"virtual",
|
|
292
|
+
"readonly",
|
|
293
|
+
"const",
|
|
294
|
+
"var",
|
|
295
|
+
"int",
|
|
296
|
+
"string",
|
|
297
|
+
"bool",
|
|
298
|
+
"double",
|
|
299
|
+
"float",
|
|
300
|
+
"object",
|
|
301
|
+
"null",
|
|
302
|
+
"true",
|
|
303
|
+
"false",
|
|
304
|
+
"if",
|
|
305
|
+
"else",
|
|
306
|
+
"for",
|
|
307
|
+
"foreach",
|
|
308
|
+
"while",
|
|
309
|
+
"do",
|
|
310
|
+
"switch",
|
|
311
|
+
"case",
|
|
312
|
+
"break",
|
|
313
|
+
"continue",
|
|
314
|
+
"try",
|
|
315
|
+
"catch",
|
|
316
|
+
"finally",
|
|
317
|
+
"throw",
|
|
318
|
+
"async",
|
|
319
|
+
"await",
|
|
320
|
+
"get",
|
|
321
|
+
"set",
|
|
322
|
+
"partial",
|
|
323
|
+
"enum",
|
|
324
|
+
"struct",
|
|
325
|
+
"delegate",
|
|
326
|
+
"event"
|
|
327
|
+
]);
|
|
328
|
+
var RUBY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
329
|
+
"def",
|
|
330
|
+
"class",
|
|
331
|
+
"module",
|
|
332
|
+
"end",
|
|
333
|
+
"do",
|
|
334
|
+
"if",
|
|
335
|
+
"else",
|
|
336
|
+
"elsif",
|
|
337
|
+
"unless",
|
|
338
|
+
"while",
|
|
339
|
+
"until",
|
|
340
|
+
"for",
|
|
341
|
+
"begin",
|
|
342
|
+
"rescue",
|
|
343
|
+
"ensure",
|
|
344
|
+
"raise",
|
|
345
|
+
"return",
|
|
346
|
+
"yield",
|
|
347
|
+
"self",
|
|
348
|
+
"super",
|
|
349
|
+
"true",
|
|
350
|
+
"false",
|
|
351
|
+
"nil",
|
|
352
|
+
"and",
|
|
353
|
+
"or",
|
|
354
|
+
"not",
|
|
355
|
+
"in",
|
|
356
|
+
"then",
|
|
357
|
+
"case",
|
|
358
|
+
"when",
|
|
359
|
+
"puts",
|
|
360
|
+
"print",
|
|
361
|
+
"require",
|
|
362
|
+
"require_relative",
|
|
363
|
+
"include",
|
|
364
|
+
"extend",
|
|
365
|
+
"attr_accessor",
|
|
366
|
+
"attr_reader",
|
|
367
|
+
"attr_writer",
|
|
368
|
+
"initialize",
|
|
369
|
+
"new",
|
|
370
|
+
"public",
|
|
371
|
+
"private",
|
|
372
|
+
"protected"
|
|
373
|
+
]);
|
|
374
|
+
var RUST_KEYWORDS = /* @__PURE__ */ new Set([
|
|
375
|
+
"fn",
|
|
376
|
+
"struct",
|
|
377
|
+
"enum",
|
|
378
|
+
"trait",
|
|
379
|
+
"impl",
|
|
380
|
+
"mod",
|
|
381
|
+
"pub",
|
|
382
|
+
"let",
|
|
383
|
+
"mut",
|
|
384
|
+
"const",
|
|
385
|
+
"type",
|
|
386
|
+
"use",
|
|
387
|
+
"crate",
|
|
388
|
+
"self",
|
|
389
|
+
"super",
|
|
390
|
+
"move",
|
|
391
|
+
"ref",
|
|
392
|
+
"match",
|
|
393
|
+
"if",
|
|
394
|
+
"else",
|
|
395
|
+
"loop",
|
|
396
|
+
"while",
|
|
397
|
+
"for",
|
|
398
|
+
"return",
|
|
399
|
+
"break",
|
|
400
|
+
"continue",
|
|
401
|
+
"where",
|
|
402
|
+
"as",
|
|
403
|
+
"in",
|
|
404
|
+
"true",
|
|
405
|
+
"false",
|
|
406
|
+
"unsafe",
|
|
407
|
+
"extern",
|
|
408
|
+
"dyn",
|
|
409
|
+
"box",
|
|
410
|
+
"static",
|
|
411
|
+
"async",
|
|
412
|
+
"await",
|
|
413
|
+
"Box",
|
|
414
|
+
"String",
|
|
415
|
+
"Vec",
|
|
416
|
+
"Option",
|
|
417
|
+
"Result",
|
|
418
|
+
"Ok",
|
|
419
|
+
"Err",
|
|
420
|
+
"Some",
|
|
421
|
+
"None"
|
|
422
|
+
]);
|
|
265
423
|
function detectLanguage(code) {
|
|
266
424
|
if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
|
|
267
425
|
if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
|
|
268
426
|
if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
|
|
269
427
|
if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
|
|
428
|
+
if (/\bnamespace\s+\w+/.test(code) || /\busing\s+System/.test(code)) return "csharp";
|
|
429
|
+
if (/\bfn\s+\w+\s*\(/.test(code) || /\blet\s+mut\s+\w+/.test(code)) return "rust";
|
|
430
|
+
if (/\bdef\s+\w+/.test(code) && /\bend\b/.test(code)) return "ruby";
|
|
270
431
|
if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
|
|
271
432
|
return "typescript";
|
|
272
433
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
for (let i = 0; i <
|
|
276
|
-
if (code
|
|
434
|
+
function buildLineIndex(code) {
|
|
435
|
+
const offsets = [];
|
|
436
|
+
for (let i = 0; i < code.length; i++) {
|
|
437
|
+
if (code.charCodeAt(i) === 10) offsets.push(i);
|
|
277
438
|
}
|
|
278
|
-
return
|
|
439
|
+
return offsets;
|
|
440
|
+
}
|
|
441
|
+
function lineFromIndex(lineOffsets, index) {
|
|
442
|
+
let lo = 0;
|
|
443
|
+
let hi = lineOffsets.length;
|
|
444
|
+
while (lo < hi) {
|
|
445
|
+
const mid = lo + hi >> 1;
|
|
446
|
+
if (lineOffsets[mid] < index) lo = mid + 1;
|
|
447
|
+
else hi = mid;
|
|
448
|
+
}
|
|
449
|
+
return lo + 1;
|
|
279
450
|
}
|
|
280
451
|
function scanTypeScript(code) {
|
|
281
452
|
const tokens = [];
|
|
282
453
|
if (!code.trim()) return tokens;
|
|
454
|
+
const lineOffsets = buildLineIndex(code);
|
|
283
455
|
const patterns = [
|
|
284
456
|
// Single-line comment
|
|
285
457
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
286
458
|
// Multi-line comment
|
|
287
459
|
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
460
|
+
// Quoted HTTP header names (fetch/axios headers objects) — mutated, not vendor-identifying literals
|
|
461
|
+
{ re: /"(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)"/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
|
|
462
|
+
{ re: /'(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)'/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
|
|
463
|
+
// Unquoted `Authorization` key — value must start with a string/template (skips interface/type shapes)
|
|
464
|
+
{
|
|
465
|
+
re: /(?:\{|\,)\s*(Authorization)\s*:\s*(?=[`'"])/g,
|
|
466
|
+
type: "HttpHeaderString" /* HttpHeaderString */,
|
|
467
|
+
groupIndex: 1
|
|
468
|
+
},
|
|
288
469
|
// Import source (the module path string)
|
|
289
470
|
{ re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
290
471
|
// Class name
|
|
@@ -292,11 +473,18 @@ function scanTypeScript(code) {
|
|
|
292
473
|
// Function/method name (function keyword)
|
|
293
474
|
{ re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
294
475
|
// 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 },
|
|
476
|
+
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
296
477
|
// Method: identifier followed by (
|
|
297
478
|
{ re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
298
|
-
//
|
|
299
|
-
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)
|
|
479
|
+
// Env-style binding names with hyphens or dots (e.g. AWS-SECRET-KEY, AWS.SECRET.KEY) — not valid JS identifiers but common in pasted config
|
|
480
|
+
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*(?:(?:-|\.)(?:[A-Za-z0-9_$]+))+)(?=\s*=)/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
481
|
+
// Variable declarations (exclude names continued with -/. so AWS-SECRET-KEY is captured only by the rule above)
|
|
482
|
+
{ re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
483
|
+
// Member receiver: matches in matches.push, row in row.patientId, def in def.name
|
|
484
|
+
{ re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\.|\?\.)[^0-9]/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
485
|
+
// Property after . or ?. when not immediately followed by ( — method calls use Method rule
|
|
486
|
+
{ re: /\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
|
|
487
|
+
{ re: /\?\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
|
|
300
488
|
// Template literals (preserve backtick wrapper)
|
|
301
489
|
{ re: /`[^`]*`/g, type: "String" /* String */ },
|
|
302
490
|
// Double-quoted strings
|
|
@@ -314,23 +502,38 @@ function scanTypeScript(code) {
|
|
|
314
502
|
if (!captureValue || captureValue.trim() === "") continue;
|
|
315
503
|
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
|
|
316
504
|
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;
|
|
505
|
+
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|Intl|WebAssembly)$/.test(captureValue)) continue;
|
|
318
506
|
if (captureValue.length <= 1) continue;
|
|
319
507
|
}
|
|
508
|
+
if (type === "Property" /* Property */ && captureValue === "meta" && code.slice(Math.max(0, captureStart - 7), captureStart) === "import.") {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
320
511
|
tokens.push({
|
|
321
512
|
type,
|
|
322
513
|
value: captureValue,
|
|
323
514
|
start: captureStart,
|
|
324
515
|
end: captureStart + captureValue.length,
|
|
325
|
-
line:
|
|
516
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
326
517
|
});
|
|
327
518
|
}
|
|
328
519
|
}
|
|
329
|
-
|
|
520
|
+
const commentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
|
|
521
|
+
const stringLikeSpans = tokens.filter(
|
|
522
|
+
(t) => t.type === "String" /* String */ || t.type === "HttpHeaderString" /* HttpHeaderString */
|
|
523
|
+
);
|
|
524
|
+
const dropsSpanOverlap = (t) => {
|
|
525
|
+
if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
|
|
526
|
+
if (commentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
|
|
527
|
+
if (stringLikeSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
};
|
|
531
|
+
return deduplicate(tokens.filter(dropsSpanOverlap));
|
|
330
532
|
}
|
|
331
533
|
function scanPython(code) {
|
|
332
534
|
const tokens = [];
|
|
333
535
|
if (!code.trim()) return tokens;
|
|
536
|
+
const lineOffsets = buildLineIndex(code);
|
|
334
537
|
const patterns = [
|
|
335
538
|
// Triple-quoted docstrings
|
|
336
539
|
{ re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
|
|
@@ -343,6 +546,11 @@ function scanPython(code) {
|
|
|
343
546
|
{ re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
344
547
|
// Function/method def
|
|
345
548
|
{ re: /\bdef\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
549
|
+
// Call site: method name in obj.method(
|
|
550
|
+
{ re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
551
|
+
// obj.attr receiver and attribute (matches.push-style)
|
|
552
|
+
{ re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?=[a-zA-Z_])/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
553
|
+
{ re: /\.([A-Za-z_][A-Za-z0-9_]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
|
|
346
554
|
// Variable assignment (simple name = ...)
|
|
347
555
|
{ re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
348
556
|
// Double-quoted strings
|
|
@@ -358,7 +566,7 @@ function scanPython(code) {
|
|
|
358
566
|
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
359
567
|
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
360
568
|
if (!captureValue || captureValue.trim() === "") continue;
|
|
361
|
-
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
|
|
569
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
|
|
362
570
|
if (PYTHON_KEYWORDS.has(captureValue)) continue;
|
|
363
571
|
if (captureValue.length <= 1) continue;
|
|
364
572
|
if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
|
|
@@ -368,15 +576,25 @@ function scanPython(code) {
|
|
|
368
576
|
value: captureValue,
|
|
369
577
|
start: captureStart,
|
|
370
578
|
end: captureStart + captureValue.length,
|
|
371
|
-
line:
|
|
579
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
372
580
|
});
|
|
373
581
|
}
|
|
374
582
|
}
|
|
375
|
-
|
|
583
|
+
const pyCommentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
|
|
584
|
+
const pyStringSpans = tokens.filter((t) => t.type === "String" /* String */);
|
|
585
|
+
const dropsPyOverlap = (t) => {
|
|
586
|
+
if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
|
|
587
|
+
if (pyCommentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
|
|
588
|
+
if (pyStringSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
};
|
|
592
|
+
return deduplicate(tokens.filter(dropsPyOverlap));
|
|
376
593
|
}
|
|
377
594
|
function scanGo(code) {
|
|
378
595
|
const tokens = [];
|
|
379
596
|
if (!code.trim()) return tokens;
|
|
597
|
+
const lineOffsets = buildLineIndex(code);
|
|
380
598
|
const patterns = [
|
|
381
599
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
382
600
|
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
@@ -413,7 +631,7 @@ function scanGo(code) {
|
|
|
413
631
|
value: captureValue,
|
|
414
632
|
start: captureStart,
|
|
415
633
|
end: captureStart + captureValue.length,
|
|
416
|
-
line:
|
|
634
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
417
635
|
});
|
|
418
636
|
}
|
|
419
637
|
}
|
|
@@ -422,6 +640,7 @@ function scanGo(code) {
|
|
|
422
640
|
function scanJava(code) {
|
|
423
641
|
const tokens = [];
|
|
424
642
|
if (!code.trim()) return tokens;
|
|
643
|
+
const lineOffsets = buildLineIndex(code);
|
|
425
644
|
const patterns = [
|
|
426
645
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
427
646
|
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
@@ -454,7 +673,137 @@ function scanJava(code) {
|
|
|
454
673
|
value: captureValue,
|
|
455
674
|
start: captureStart,
|
|
456
675
|
end: captureStart + captureValue.length,
|
|
457
|
-
line:
|
|
676
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return deduplicate(tokens);
|
|
681
|
+
}
|
|
682
|
+
function scanCSharp(code) {
|
|
683
|
+
const tokens = [];
|
|
684
|
+
if (!code.trim()) return tokens;
|
|
685
|
+
const lineOffsets = buildLineIndex(code);
|
|
686
|
+
const patterns = [
|
|
687
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
688
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
689
|
+
// Namespace/using import
|
|
690
|
+
{ re: /\busing\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
691
|
+
// Class/interface/struct/enum names
|
|
692
|
+
{ re: /\b(?:class|interface|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
693
|
+
// Method/property declaration
|
|
694
|
+
{ re: /\b(?:public|private|protected|internal|static|virtual|override|async)\s+(?:\w+\s+)+([a-zA-Z_]\w*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
695
|
+
// Local variable declarations
|
|
696
|
+
{ re: /\b(?:var|int|string|bool|double|float|object)\s+([a-zA-Z_]\w*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
697
|
+
// Double-quoted strings
|
|
698
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
699
|
+
// Verbatim strings
|
|
700
|
+
{ re: /@"(?:[^"]|"")*"/g, type: "String" /* String */ }
|
|
701
|
+
];
|
|
702
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
703
|
+
re.lastIndex = 0;
|
|
704
|
+
let m;
|
|
705
|
+
while ((m = re.exec(code)) !== null) {
|
|
706
|
+
const fullMatch = m[0];
|
|
707
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
708
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
709
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
710
|
+
if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
|
|
711
|
+
if (CSHARP_KEYWORDS.has(captureValue)) continue;
|
|
712
|
+
if (captureValue.length <= 1) continue;
|
|
713
|
+
if (/^(String|Object|Integer|Boolean|Double|Float|List|Dictionary|Array|Exception|Task|Console|Math|Enum|Delegate)$/.test(captureValue)) continue;
|
|
714
|
+
}
|
|
715
|
+
tokens.push({
|
|
716
|
+
type,
|
|
717
|
+
value: captureValue,
|
|
718
|
+
start: captureStart,
|
|
719
|
+
end: captureStart + captureValue.length,
|
|
720
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return deduplicate(tokens);
|
|
725
|
+
}
|
|
726
|
+
function scanRuby(code) {
|
|
727
|
+
const tokens = [];
|
|
728
|
+
if (!code.trim()) return tokens;
|
|
729
|
+
const lineOffsets = buildLineIndex(code);
|
|
730
|
+
const patterns = [
|
|
731
|
+
// Single-line comment
|
|
732
|
+
{ re: /#[^\n]*/g, type: "Comment" /* Comment */ },
|
|
733
|
+
// Method names
|
|
734
|
+
{ re: /\bdef\s+([a-zA-Z_]\w*[!?]?)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
735
|
+
// Class and module names
|
|
736
|
+
{ re: /\b(?:class|module)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
737
|
+
// Instance variables
|
|
738
|
+
{ re: /@([a-zA-Z_]\w*)/g, type: "Property" /* Property */, groupIndex: 1 },
|
|
739
|
+
// All-caps constants (3+ chars to avoid single-letter constants)
|
|
740
|
+
{ re: /\b([A-Z][A-Z0-9_]{2,})\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
741
|
+
// Double-quoted strings
|
|
742
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
743
|
+
// Single-quoted strings
|
|
744
|
+
{ re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
|
|
745
|
+
];
|
|
746
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
747
|
+
re.lastIndex = 0;
|
|
748
|
+
let m;
|
|
749
|
+
while ((m = re.exec(code)) !== null) {
|
|
750
|
+
const fullMatch = m[0];
|
|
751
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
752
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
753
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
754
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Property" /* Property */) {
|
|
755
|
+
if (RUBY_KEYWORDS.has(captureValue)) continue;
|
|
756
|
+
if (captureValue.length <= 1) continue;
|
|
757
|
+
}
|
|
758
|
+
tokens.push({
|
|
759
|
+
type,
|
|
760
|
+
value: captureValue,
|
|
761
|
+
start: captureStart,
|
|
762
|
+
end: captureStart + captureValue.length,
|
|
763
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return deduplicate(tokens);
|
|
768
|
+
}
|
|
769
|
+
function scanRust(code) {
|
|
770
|
+
const tokens = [];
|
|
771
|
+
if (!code.trim()) return tokens;
|
|
772
|
+
const lineOffsets = buildLineIndex(code);
|
|
773
|
+
const patterns = [
|
|
774
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
775
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
776
|
+
// Use declarations
|
|
777
|
+
{ re: /\buse\s+([\w:]+)/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
778
|
+
// Named type declarations (struct, enum, trait)
|
|
779
|
+
{ re: /\b(?:struct|enum|trait)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
780
|
+
// Function names
|
|
781
|
+
{ re: /\bfn\s+([a-zA-Z_]\w*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
782
|
+
// Constants (ALL_CAPS)
|
|
783
|
+
{ re: /\bconst\s+([A-Z_][A-Z0-9_]*)\s*:/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
784
|
+
// Variable bindings
|
|
785
|
+
{ re: /\blet\s+(?:mut\s+)?([a-zA-Z_]\w*)/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
786
|
+
// Double-quoted strings
|
|
787
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
|
|
788
|
+
];
|
|
789
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
790
|
+
re.lastIndex = 0;
|
|
791
|
+
let m;
|
|
792
|
+
while ((m = re.exec(code)) !== null) {
|
|
793
|
+
const fullMatch = m[0];
|
|
794
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
795
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
796
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
797
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
|
|
798
|
+
if (RUST_KEYWORDS.has(captureValue)) continue;
|
|
799
|
+
if (captureValue.length <= 1) continue;
|
|
800
|
+
}
|
|
801
|
+
tokens.push({
|
|
802
|
+
type,
|
|
803
|
+
value: captureValue,
|
|
804
|
+
start: captureStart,
|
|
805
|
+
end: captureStart + captureValue.length,
|
|
806
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
458
807
|
});
|
|
459
808
|
}
|
|
460
809
|
}
|
|
@@ -498,13 +847,62 @@ function scan(code, language) {
|
|
|
498
847
|
case "java":
|
|
499
848
|
tokens = scanJava(code);
|
|
500
849
|
break;
|
|
850
|
+
case "csharp":
|
|
851
|
+
case "cs":
|
|
852
|
+
tokens = scanCSharp(code);
|
|
853
|
+
break;
|
|
854
|
+
case "ruby":
|
|
855
|
+
case "rb":
|
|
856
|
+
tokens = scanRuby(code);
|
|
857
|
+
break;
|
|
858
|
+
case "rust":
|
|
859
|
+
case "rs":
|
|
860
|
+
tokens = scanRust(code);
|
|
861
|
+
break;
|
|
501
862
|
default:
|
|
502
863
|
tokens = scanTypeScript(code);
|
|
503
864
|
}
|
|
504
865
|
return { tokens, language: lang };
|
|
505
866
|
}
|
|
506
867
|
|
|
507
|
-
// src/
|
|
868
|
+
// src/salt.ts
|
|
869
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
870
|
+
import { join } from "path";
|
|
871
|
+
import { homedir } from "os";
|
|
872
|
+
import { randomBytes } from "crypto";
|
|
873
|
+
var SALT_BYTES = 32;
|
|
874
|
+
function getSaltPath() {
|
|
875
|
+
const dir = join(homedir(), ".pretense");
|
|
876
|
+
return { dir, path: join(dir, "mutation.salt") };
|
|
877
|
+
}
|
|
878
|
+
var _cachedSalt = null;
|
|
879
|
+
function getMutationSalt() {
|
|
880
|
+
if (_cachedSalt) return _cachedSalt;
|
|
881
|
+
const { dir, path } = getSaltPath();
|
|
882
|
+
if (existsSync(path)) {
|
|
883
|
+
try {
|
|
884
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
885
|
+
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
886
|
+
_cachedSalt = raw.toLowerCase();
|
|
887
|
+
return _cachedSalt;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const salt = randomBytes(SALT_BYTES).toString("hex");
|
|
893
|
+
try {
|
|
894
|
+
mkdirSync(dir, { recursive: true });
|
|
895
|
+
writeFileSync(path, salt, { mode: 384 });
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
_cachedSalt = salt;
|
|
899
|
+
return salt;
|
|
900
|
+
}
|
|
901
|
+
function buildSaltedSeed(baseSeed) {
|
|
902
|
+
return `${baseSeed}:${getMutationSalt()}`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/deterministic-id.ts
|
|
508
906
|
function hash32(str) {
|
|
509
907
|
let h = 5381;
|
|
510
908
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -522,11 +920,13 @@ function toAlphaNum(n, len) {
|
|
|
522
920
|
}
|
|
523
921
|
return result;
|
|
524
922
|
}
|
|
525
|
-
function
|
|
923
|
+
function deterministicSyntheticId(original, seed, prefix, len = 4) {
|
|
526
924
|
const combined = `${seed}:${prefix}:${original}`;
|
|
527
925
|
const h = hash32(combined);
|
|
528
926
|
return prefix + toAlphaNum(h, len);
|
|
529
927
|
}
|
|
928
|
+
|
|
929
|
+
// src/mutator.ts
|
|
530
930
|
function prefixForType(type) {
|
|
531
931
|
switch (type) {
|
|
532
932
|
case "Class" /* Class */:
|
|
@@ -538,6 +938,8 @@ function prefixForType(type) {
|
|
|
538
938
|
return "_v";
|
|
539
939
|
case "Property" /* Property */:
|
|
540
940
|
return "_prop";
|
|
941
|
+
case "HttpHeaderString" /* HttpHeaderString */:
|
|
942
|
+
return "_hk";
|
|
541
943
|
default:
|
|
542
944
|
return "_tok";
|
|
543
945
|
}
|
|
@@ -553,6 +955,7 @@ function mutate(code, language, seed = "pretense") {
|
|
|
553
955
|
stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
|
|
554
956
|
};
|
|
555
957
|
}
|
|
958
|
+
const effectiveSeed = seed === "pretense" ? buildSaltedSeed(seed) : seed;
|
|
556
959
|
const { tokens } = scan(code, lang);
|
|
557
960
|
const map = /* @__PURE__ */ new Map();
|
|
558
961
|
for (const token of tokens) {
|
|
@@ -563,11 +966,24 @@ function mutate(code, language, seed = "pretense") {
|
|
|
563
966
|
if (token.type === "String" /* String */) {
|
|
564
967
|
continue;
|
|
565
968
|
}
|
|
969
|
+
if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
|
|
970
|
+
const qm = /^("|')(.+)\1$/.exec(token.value);
|
|
971
|
+
if (qm) {
|
|
972
|
+
const inner = qm[2];
|
|
973
|
+
const q = qm[1];
|
|
974
|
+
const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
|
|
975
|
+
map.set(token.value, `${q}${syntheticInner}${q}`);
|
|
976
|
+
} else {
|
|
977
|
+
const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
|
|
978
|
+
map.set(token.value, syntheticInner);
|
|
979
|
+
}
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
566
982
|
if (token.type === "Import" /* Import */) {
|
|
567
983
|
continue;
|
|
568
984
|
}
|
|
569
985
|
const prefix = prefixForType(token.type);
|
|
570
|
-
const synthetic =
|
|
986
|
+
const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
|
|
571
987
|
map.set(token.value, synthetic);
|
|
572
988
|
}
|
|
573
989
|
const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
@@ -595,22 +1011,31 @@ function mutate(code, language, seed = "pretense") {
|
|
|
595
1011
|
}
|
|
596
1012
|
|
|
597
1013
|
// src/reverser.ts
|
|
598
|
-
function reverse(mutatedCode, map) {
|
|
599
|
-
if (!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);
|
|
1014
|
+
function reverse(mutatedCode, map, secretsMap) {
|
|
1015
|
+
if (!mutatedCode) return mutatedCode;
|
|
607
1016
|
let result = mutatedCode;
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1017
|
+
if (map.size > 0) {
|
|
1018
|
+
const reverseMap = /* @__PURE__ */ new Map();
|
|
1019
|
+
for (const [original, synthetic] of map.entries()) {
|
|
1020
|
+
if (synthetic === "" || !synthetic) continue;
|
|
1021
|
+
reverseMap.set(synthetic, original);
|
|
1022
|
+
}
|
|
1023
|
+
if (reverseMap.size > 0) {
|
|
1024
|
+
const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
1025
|
+
for (const [synthetic, original] of sortedEntries) {
|
|
1026
|
+
const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1027
|
+
if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
|
|
1028
|
+
result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
|
|
1029
|
+
} else {
|
|
1030
|
+
result = result.split(synthetic).join(original);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (secretsMap && secretsMap.size > 0) {
|
|
1036
|
+
const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
1037
|
+
for (const [placeholder, original] of sortedSecrets) {
|
|
1038
|
+
result = result.split(placeholder).join(original);
|
|
614
1039
|
}
|
|
615
1040
|
}
|
|
616
1041
|
return result;
|
|
@@ -619,16 +1044,40 @@ function reverse(mutatedCode, map) {
|
|
|
619
1044
|
// src/secrets.ts
|
|
620
1045
|
var SECRET_PATTERNS = [
|
|
621
1046
|
{ name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
|
|
622
|
-
|
|
1047
|
+
/** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with @pretense/scanner */
|
|
1048
|
+
{ name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
|
|
623
1049
|
{ name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
624
|
-
|
|
1050
|
+
/** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
|
|
1051
|
+
{ name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
|
|
1052
|
+
/**
|
|
1053
|
+
* Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
|
|
1054
|
+
* Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
|
|
1055
|
+
*/
|
|
1056
|
+
{
|
|
1057
|
+
name: "aws-secret-key-delimited",
|
|
1058
|
+
category: "secret",
|
|
1059
|
+
severity: "critical",
|
|
1060
|
+
defaultAction: "block",
|
|
1061
|
+
pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
|
|
1062
|
+
valueGroup: 1
|
|
1063
|
+
},
|
|
1064
|
+
{ 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})['"]?/gd, valueGroup: 1 },
|
|
625
1065
|
{ name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
|
|
626
1066
|
{ name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
|
|
627
1067
|
{ 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
|
-
|
|
1068
|
+
{ name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
1069
|
+
/** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
|
|
1070
|
+
{ name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{4,}/g },
|
|
630
1071
|
{ name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
|
|
631
|
-
{
|
|
1072
|
+
{
|
|
1073
|
+
name: "generic-password",
|
|
1074
|
+
category: "credential",
|
|
1075
|
+
severity: "high",
|
|
1076
|
+
defaultAction: "redact",
|
|
1077
|
+
// Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
|
|
1078
|
+
pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
|
|
1079
|
+
valueGroup: 1
|
|
1080
|
+
},
|
|
632
1081
|
{ name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
|
|
633
1082
|
{ name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
|
|
634
1083
|
{ name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
|
|
@@ -654,7 +1103,36 @@ var SECRET_PATTERNS = [
|
|
|
654
1103
|
{ name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
|
|
655
1104
|
{ name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
|
|
656
1105
|
{ 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 }
|
|
1106
|
+
{ name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g },
|
|
1107
|
+
// ─── Extended patterns (v0.6 unit 8) ─────────────────────────────────────────
|
|
1108
|
+
{ name: "google-oauth-refresh", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/g },
|
|
1109
|
+
{ name: "slack-webhook-url", category: "secret", severity: "critical", defaultAction: "block", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]{20,}/g },
|
|
1110
|
+
{ name: "sentry-dsn", category: "secret", severity: "high", defaultAction: "redact", pattern: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.sentry\.io\/\d+/g },
|
|
1111
|
+
{ name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
|
|
1112
|
+
{ name: "launchdarkly-sdk-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bsdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
|
|
1113
|
+
{ name: "launchdarkly-mobile-key", category: "secret", severity: "high", defaultAction: "block", pattern: /\bmob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
|
|
1114
|
+
/** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
|
|
1115
|
+
{ name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
|
|
1116
|
+
/** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
|
|
1117
|
+
{ name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
|
|
1118
|
+
/** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
|
|
1119
|
+
{
|
|
1120
|
+
name: "env-password-assignment",
|
|
1121
|
+
category: "credential",
|
|
1122
|
+
severity: "high",
|
|
1123
|
+
defaultAction: "block",
|
|
1124
|
+
pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
|
|
1125
|
+
valueGroup: 1
|
|
1126
|
+
},
|
|
1127
|
+
/** Quoted common DB dialect names — stack hints when mutating env-style config. */
|
|
1128
|
+
{
|
|
1129
|
+
name: "quoted-db-dialect",
|
|
1130
|
+
category: "credential",
|
|
1131
|
+
severity: "medium",
|
|
1132
|
+
defaultAction: "block",
|
|
1133
|
+
pattern: /(['"])(postgres|mysql|redis)\1/gd,
|
|
1134
|
+
valueGroup: 2
|
|
1135
|
+
}
|
|
658
1136
|
];
|
|
659
1137
|
var PII_PATTERNS = [
|
|
660
1138
|
{ name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
@@ -712,6 +1190,7 @@ function maskValue(value, type) {
|
|
|
712
1190
|
return "***";
|
|
713
1191
|
}
|
|
714
1192
|
var matchCounter = 0;
|
|
1193
|
+
var ENTROPY_SCAN_MAX_BYTES = 512 * 1024;
|
|
715
1194
|
function scan2(text, actionOverrides) {
|
|
716
1195
|
const start = performance.now();
|
|
717
1196
|
const matches = [];
|
|
@@ -720,7 +1199,17 @@ function scan2(text, actionOverrides) {
|
|
|
720
1199
|
def.pattern.lastIndex = 0;
|
|
721
1200
|
let m;
|
|
722
1201
|
while ((m = def.pattern.exec(text)) !== null) {
|
|
723
|
-
|
|
1202
|
+
let value = m[0];
|
|
1203
|
+
let start2 = m.index;
|
|
1204
|
+
let end = start2 + value.length;
|
|
1205
|
+
if (def.valueGroup !== void 0) {
|
|
1206
|
+
const span = m.indices?.[def.valueGroup];
|
|
1207
|
+
if (!span) continue;
|
|
1208
|
+
const [gs, ge] = span;
|
|
1209
|
+
value = text.slice(gs, ge);
|
|
1210
|
+
start2 = gs;
|
|
1211
|
+
end = ge;
|
|
1212
|
+
}
|
|
724
1213
|
if (def.validate && !def.validate(value)) continue;
|
|
725
1214
|
const action = actionOverrides?.[def.name] ?? def.defaultAction;
|
|
726
1215
|
matchCounter++;
|
|
@@ -730,35 +1219,51 @@ function scan2(text, actionOverrides) {
|
|
|
730
1219
|
type: def.name,
|
|
731
1220
|
value,
|
|
732
1221
|
masked: maskValue(value, def.name),
|
|
733
|
-
start:
|
|
734
|
-
end
|
|
1222
|
+
start: start2,
|
|
1223
|
+
end,
|
|
735
1224
|
severity: def.severity,
|
|
736
1225
|
action
|
|
737
1226
|
});
|
|
738
1227
|
}
|
|
739
1228
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1229
|
+
if (text.length <= ENTROPY_SCAN_MAX_BYTES) {
|
|
1230
|
+
const sortedByStart = [...matches].sort((a, b) => a.start - b.start);
|
|
1231
|
+
const coveredEnd = (idx) => {
|
|
1232
|
+
let lo = 0;
|
|
1233
|
+
let hi = sortedByStart.length - 1;
|
|
1234
|
+
let best = -1;
|
|
1235
|
+
while (lo <= hi) {
|
|
1236
|
+
const mid = lo + hi >> 1;
|
|
1237
|
+
if (sortedByStart[mid].start <= idx) {
|
|
1238
|
+
best = mid;
|
|
1239
|
+
lo = mid + 1;
|
|
1240
|
+
} else {
|
|
1241
|
+
hi = mid - 1;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return best >= 0 ? sortedByStart[best].end : -1;
|
|
1245
|
+
};
|
|
1246
|
+
const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
|
|
1247
|
+
highEntropyPattern.lastIndex = 0;
|
|
1248
|
+
let hem;
|
|
1249
|
+
while ((hem = highEntropyPattern.exec(text)) !== null) {
|
|
1250
|
+
const value = hem[0];
|
|
1251
|
+
const end = hem.index + value.length;
|
|
1252
|
+
if (coveredEnd(hem.index) >= end) continue;
|
|
1253
|
+
if (isHighEntropy(value)) {
|
|
1254
|
+
matchCounter++;
|
|
1255
|
+
matches.push({
|
|
1256
|
+
id: `scan_${matchCounter}`,
|
|
1257
|
+
category: "secret",
|
|
1258
|
+
type: "high-entropy-string",
|
|
1259
|
+
value,
|
|
1260
|
+
masked: value.slice(0, 6) + "..." + value.slice(-4),
|
|
1261
|
+
start: hem.index,
|
|
1262
|
+
end,
|
|
1263
|
+
severity: "medium",
|
|
1264
|
+
action: actionOverrides?.["high-entropy-string"] ?? "warn"
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
762
1267
|
}
|
|
763
1268
|
}
|
|
764
1269
|
const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
@@ -779,18 +1284,30 @@ function scan2(text, actionOverrides) {
|
|
|
779
1284
|
};
|
|
780
1285
|
}
|
|
781
1286
|
function applyRedactions(text, matches) {
|
|
782
|
-
|
|
1287
|
+
return applyRedactionsTracked(text, matches).redactedCode;
|
|
1288
|
+
}
|
|
1289
|
+
var SECRET_LITERAL_PREFIX = "_sl";
|
|
1290
|
+
function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
|
|
1291
|
+
const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
|
|
1292
|
+
const sorted = [...actionable].sort((a, b) => b.start - a.start);
|
|
1293
|
+
const secretsMap = /* @__PURE__ */ new Map();
|
|
1294
|
+
const placeholders = /* @__PURE__ */ new Map();
|
|
1295
|
+
const forward = [...actionable].sort((a, b) => a.start - b.start);
|
|
1296
|
+
for (const match of forward) {
|
|
1297
|
+
const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
|
|
1298
|
+
placeholders.set(match.id, tag);
|
|
1299
|
+
secretsMap.set(tag, match.value);
|
|
1300
|
+
}
|
|
783
1301
|
let result = text;
|
|
784
1302
|
for (const match of sorted) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
1303
|
+
const tag = placeholders.get(match.id);
|
|
1304
|
+
result = result.slice(0, match.start) + tag + result.slice(match.end);
|
|
788
1305
|
}
|
|
789
|
-
return result;
|
|
1306
|
+
return { redactedCode: result, secretsMap };
|
|
790
1307
|
}
|
|
791
1308
|
|
|
792
1309
|
// src/store.ts
|
|
793
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1310
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
794
1311
|
import { dirname } from "path";
|
|
795
1312
|
var MutationStore = class {
|
|
796
1313
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -855,20 +1372,20 @@ var MutationStore = class {
|
|
|
855
1372
|
*/
|
|
856
1373
|
persist() {
|
|
857
1374
|
const dir = dirname(this.filePath);
|
|
858
|
-
if (!
|
|
859
|
-
|
|
1375
|
+
if (!existsSync2(dir)) {
|
|
1376
|
+
mkdirSync2(dir, { recursive: true });
|
|
860
1377
|
}
|
|
861
1378
|
const data = JSON.stringify([...this.entries.values()], null, 2);
|
|
862
|
-
|
|
1379
|
+
writeFileSync2(this.filePath, data, "utf-8");
|
|
863
1380
|
}
|
|
864
1381
|
/**
|
|
865
1382
|
* Load entries from the JSON file into memory.
|
|
866
1383
|
* No-ops if the file doesn't exist.
|
|
867
1384
|
*/
|
|
868
1385
|
load() {
|
|
869
|
-
if (!
|
|
1386
|
+
if (!existsSync2(this.filePath)) return;
|
|
870
1387
|
try {
|
|
871
|
-
const raw =
|
|
1388
|
+
const raw = readFileSync2(this.filePath, "utf-8");
|
|
872
1389
|
const parsed = JSON.parse(raw);
|
|
873
1390
|
this.entries.clear();
|
|
874
1391
|
for (const entry of parsed) {
|
|
@@ -901,8 +1418,9 @@ var MutationStore = class {
|
|
|
901
1418
|
};
|
|
902
1419
|
|
|
903
1420
|
// src/config.ts
|
|
904
|
-
import { existsSync as
|
|
905
|
-
import { join, resolve } from "path";
|
|
1421
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1422
|
+
import { join as join2, resolve } from "path";
|
|
1423
|
+
import { createHmac } from "crypto";
|
|
906
1424
|
var CONFIG_DIR_NAME = ".pretense";
|
|
907
1425
|
var CONFIG_FILE = "config.json";
|
|
908
1426
|
var MUTATION_MAP_FILE = "mutation-map.json";
|
|
@@ -914,30 +1432,33 @@ function getConfigDir(baseDir) {
|
|
|
914
1432
|
}
|
|
915
1433
|
function initConfig(dir) {
|
|
916
1434
|
const configDir = getConfigDir(dir);
|
|
917
|
-
if (!
|
|
918
|
-
|
|
1435
|
+
if (!existsSync3(configDir)) {
|
|
1436
|
+
mkdirSync3(configDir, { recursive: true });
|
|
919
1437
|
}
|
|
920
|
-
const configPath =
|
|
921
|
-
if (!
|
|
1438
|
+
const configPath = join2(configDir, CONFIG_FILE);
|
|
1439
|
+
if (!existsSync3(configPath)) {
|
|
922
1440
|
const config = {
|
|
923
1441
|
...DEFAULT_CONFIG,
|
|
924
1442
|
storePath: configDir
|
|
925
1443
|
};
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
const mapPath = join(configDir, MUTATION_MAP_FILE);
|
|
929
|
-
if (!existsSync2(mapPath)) {
|
|
930
|
-
writeFileSync2(mapPath, "[]", "utf-8");
|
|
1444
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
931
1445
|
}
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
934
|
-
|
|
1446
|
+
const mapPath = join2(configDir, MUTATION_MAP_FILE);
|
|
1447
|
+
if (!existsSync3(mapPath)) {
|
|
1448
|
+
writeFileSync3(mapPath, "[]", "utf-8");
|
|
935
1449
|
}
|
|
936
|
-
const
|
|
937
|
-
if (!
|
|
938
|
-
|
|
1450
|
+
const auditPath = join2(configDir, AUDIT_LOG_FILE);
|
|
1451
|
+
if (!existsSync3(auditPath)) {
|
|
1452
|
+
writeFileSync3(auditPath, "", "utf-8");
|
|
1453
|
+
}
|
|
1454
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
1455
|
+
if (!existsSync3(usagePath)) {
|
|
1456
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1457
|
+
const month = today.slice(0, 7);
|
|
1458
|
+
const usageData = { month, mutations: 0, firstUseDate: today };
|
|
1459
|
+
writeFileSync3(
|
|
939
1460
|
usagePath,
|
|
940
|
-
JSON.stringify({
|
|
1461
|
+
JSON.stringify({ ...usageData, date: today, checksum: computeUsageChecksum(usageData) }, null, 2),
|
|
941
1462
|
"utf-8"
|
|
942
1463
|
);
|
|
943
1464
|
}
|
|
@@ -945,71 +1466,116 @@ function initConfig(dir) {
|
|
|
945
1466
|
}
|
|
946
1467
|
function loadConfig(dir) {
|
|
947
1468
|
const configDir = getConfigDir(dir);
|
|
948
|
-
const configPath =
|
|
949
|
-
if (!
|
|
1469
|
+
const configPath = join2(configDir, CONFIG_FILE);
|
|
1470
|
+
if (!existsSync3(configPath)) {
|
|
950
1471
|
return { ...DEFAULT_CONFIG };
|
|
951
1472
|
}
|
|
952
1473
|
try {
|
|
953
|
-
const raw =
|
|
1474
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
954
1475
|
const parsed = JSON.parse(raw);
|
|
955
1476
|
return { ...DEFAULT_CONFIG, ...parsed };
|
|
956
1477
|
} catch {
|
|
957
1478
|
return { ...DEFAULT_CONFIG };
|
|
958
1479
|
}
|
|
959
1480
|
}
|
|
1481
|
+
var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
|
|
1482
|
+
function getUsageSecret() {
|
|
1483
|
+
return process.env["PRETENSE_USAGE_SECRET"] ?? USAGE_HMAC_SECRET;
|
|
1484
|
+
}
|
|
1485
|
+
function computeUsageChecksum(data) {
|
|
1486
|
+
const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
|
|
1487
|
+
return createHmac("sha256", getUsageSecret()).update(payload).digest("hex");
|
|
1488
|
+
}
|
|
1489
|
+
function verifyUsageChecksum(data) {
|
|
1490
|
+
if (!data.checksum) return false;
|
|
1491
|
+
return data.checksum === computeUsageChecksum(data);
|
|
1492
|
+
}
|
|
960
1493
|
function loadUsage(dir) {
|
|
961
1494
|
const configDir = getConfigDir(dir);
|
|
962
|
-
const usagePath =
|
|
1495
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
963
1496
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
964
|
-
|
|
965
|
-
|
|
1497
|
+
const currentMonth = today.slice(0, 7);
|
|
1498
|
+
if (!existsSync3(usagePath)) {
|
|
1499
|
+
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
966
1500
|
}
|
|
967
1501
|
try {
|
|
968
|
-
const raw =
|
|
1502
|
+
const raw = readFileSync3(usagePath, "utf-8");
|
|
969
1503
|
const data = JSON.parse(raw);
|
|
970
|
-
if (data.date
|
|
971
|
-
|
|
1504
|
+
if (data.date && !data.month) {
|
|
1505
|
+
const legacyDate = data.date;
|
|
1506
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
|
|
972
1507
|
}
|
|
973
|
-
|
|
1508
|
+
if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
|
|
1509
|
+
process.stderr.write("[PRETENSE] Warning: usage.json integrity check failed. Resetting usage counter.\n");
|
|
1510
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
1511
|
+
}
|
|
1512
|
+
if (data.month !== currentMonth) {
|
|
1513
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
1514
|
+
}
|
|
1515
|
+
return { month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate };
|
|
974
1516
|
} catch {
|
|
975
|
-
return {
|
|
1517
|
+
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
976
1518
|
}
|
|
977
1519
|
}
|
|
978
1520
|
function saveUsage(usage, dir) {
|
|
979
1521
|
const configDir = getConfigDir(dir);
|
|
980
|
-
if (!
|
|
981
|
-
|
|
1522
|
+
if (!existsSync3(configDir)) {
|
|
1523
|
+
mkdirSync3(configDir, { recursive: true });
|
|
982
1524
|
}
|
|
983
|
-
const usagePath =
|
|
984
|
-
|
|
1525
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
1526
|
+
const checksum = computeUsageChecksum(usage);
|
|
1527
|
+
writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
|
|
1528
|
+
}
|
|
1529
|
+
var MIN_LICENSE_KEY_LENGTH = 40;
|
|
1530
|
+
var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
|
|
1531
|
+
function isValidLicenseKey(key, prefix) {
|
|
1532
|
+
if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
|
|
1533
|
+
const afterPrefix = key.slice(prefix.length);
|
|
1534
|
+
const hmacSecret = process.env["PRETENSE_LICENSE_SECRET"];
|
|
1535
|
+
if (hmacSecret) {
|
|
1536
|
+
const lastUnderscore = afterPrefix.lastIndexOf("_");
|
|
1537
|
+
if (lastUnderscore === -1) return false;
|
|
1538
|
+
const payload = afterPrefix.slice(0, lastUnderscore);
|
|
1539
|
+
const providedHmac = afterPrefix.slice(lastUnderscore + 1);
|
|
1540
|
+
if (!payload || !providedHmac) return false;
|
|
1541
|
+
if (!LICENSE_PAYLOAD_REGEX.test(payload)) return false;
|
|
1542
|
+
const expectedHmac = createHmac("sha256", hmacSecret).update(payload).digest("hex").slice(0, 16);
|
|
1543
|
+
return providedHmac === expectedHmac;
|
|
1544
|
+
}
|
|
1545
|
+
return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
|
|
985
1546
|
}
|
|
986
1547
|
function detectTier() {
|
|
987
1548
|
const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
|
|
988
|
-
if (key.startsWith("pre_ent_")) return "enterprise";
|
|
989
|
-
if (key.startsWith("pre_pro_")) return "pro";
|
|
1549
|
+
if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
|
|
1550
|
+
if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
|
|
990
1551
|
return "free";
|
|
991
1552
|
}
|
|
1553
|
+
function tierFromDashboardPlan(plan, fallback) {
|
|
1554
|
+
if (plan === "ENTERPRISE") return "enterprise";
|
|
1555
|
+
if (plan === "PRO") return "pro";
|
|
1556
|
+
return fallback;
|
|
1557
|
+
}
|
|
992
1558
|
function getTierLimits(tier) {
|
|
993
1559
|
switch (tier) {
|
|
994
1560
|
case "enterprise":
|
|
995
|
-
return {
|
|
1561
|
+
return { monthlyMutations: Infinity, auditRetentionDays: Infinity };
|
|
996
1562
|
case "pro":
|
|
997
|
-
return {
|
|
1563
|
+
return { monthlyMutations: Infinity, auditRetentionDays: 90 };
|
|
998
1564
|
case "free":
|
|
999
1565
|
default:
|
|
1000
|
-
return {
|
|
1566
|
+
return { monthlyMutations: 1e3, auditRetentionDays: 30 };
|
|
1001
1567
|
}
|
|
1002
1568
|
}
|
|
1003
1569
|
|
|
1004
1570
|
// src/audit.ts
|
|
1005
|
-
import { appendFileSync, existsSync as
|
|
1006
|
-
import { join as
|
|
1571
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
1572
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1007
1573
|
function writeAuditEntry(entry, dir) {
|
|
1008
1574
|
const configDir = getConfigDir(dir);
|
|
1009
|
-
const auditPath =
|
|
1575
|
+
const auditPath = join3(configDir, "audit.log");
|
|
1010
1576
|
const logDir = dirname2(auditPath);
|
|
1011
|
-
if (!
|
|
1012
|
-
|
|
1577
|
+
if (!existsSync4(logDir)) {
|
|
1578
|
+
mkdirSync4(logDir, { recursive: true });
|
|
1013
1579
|
}
|
|
1014
1580
|
const line = JSON.stringify(entry) + "\n";
|
|
1015
1581
|
appendFileSync(auditPath, line, "utf-8");
|
|
@@ -1028,11 +1594,11 @@ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, l
|
|
|
1028
1594
|
function readAuditLog(options = {}) {
|
|
1029
1595
|
const { limit, dir } = options;
|
|
1030
1596
|
const configDir = getConfigDir(dir);
|
|
1031
|
-
const auditPath =
|
|
1032
|
-
if (!
|
|
1597
|
+
const auditPath = join3(configDir, "audit.log");
|
|
1598
|
+
if (!existsSync4(auditPath)) {
|
|
1033
1599
|
return [];
|
|
1034
1600
|
}
|
|
1035
|
-
const raw =
|
|
1601
|
+
const raw = readFileSync4(auditPath, "utf-8");
|
|
1036
1602
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
1037
1603
|
let entries = [];
|
|
1038
1604
|
for (const line of lines) {
|
|
@@ -1095,18 +1661,758 @@ function formatAudit(entries, format = "text") {
|
|
|
1095
1661
|
import { Hono } from "hono";
|
|
1096
1662
|
import { serve } from "@hono/node-server";
|
|
1097
1663
|
import { nanoid } from "nanoid";
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1664
|
+
import { createServer } from "net";
|
|
1665
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
1666
|
+
|
|
1667
|
+
// src/auth.ts
|
|
1668
|
+
import { createHash } from "crypto";
|
|
1669
|
+
var AUTH_HEADERS = ["authorization", "x-api-key", "x-goog-api-key"];
|
|
1670
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
1671
|
+
"/",
|
|
1672
|
+
"/health",
|
|
1673
|
+
"/stats",
|
|
1674
|
+
"/audit",
|
|
1675
|
+
"/tokens"
|
|
1676
|
+
]);
|
|
1677
|
+
function extractApiKey(c) {
|
|
1678
|
+
for (const h of AUTH_HEADERS) {
|
|
1679
|
+
const v = c.req.header(h);
|
|
1680
|
+
if (v && v.length > 0) {
|
|
1681
|
+
return v.replace(/^Bearer\s+/i, "").trim();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
function sessionHash(apiKey) {
|
|
1687
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
1688
|
+
}
|
|
1689
|
+
var requireApiKey = async (c, next) => {
|
|
1690
|
+
if (PUBLIC_PATHS.has(c.req.path)) {
|
|
1691
|
+
return next();
|
|
1692
|
+
}
|
|
1693
|
+
const key = extractApiKey(c);
|
|
1694
|
+
if (!key) {
|
|
1695
|
+
return c.json(
|
|
1696
|
+
{
|
|
1697
|
+
error: {
|
|
1698
|
+
type: "missing_api_key",
|
|
1699
|
+
message: "Authorization header required (Authorization, x-api-key, or x-goog-api-key)"
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1702
|
+
401
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
c.set("sessionHash", sessionHash(key));
|
|
1706
|
+
return next();
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// src/session-store.ts
|
|
1710
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1711
|
+
function getSession(hash) {
|
|
1712
|
+
let s = sessions.get(hash);
|
|
1713
|
+
if (!s) {
|
|
1714
|
+
s = {
|
|
1715
|
+
mutationMap: /* @__PURE__ */ new Map(),
|
|
1716
|
+
reverseMap: /* @__PURE__ */ new Map(),
|
|
1717
|
+
createdAt: Date.now(),
|
|
1718
|
+
lastUsed: Date.now()
|
|
1719
|
+
};
|
|
1720
|
+
sessions.set(hash, s);
|
|
1721
|
+
}
|
|
1722
|
+
s.lastUsed = Date.now();
|
|
1723
|
+
return s;
|
|
1724
|
+
}
|
|
1725
|
+
function sessionCount() {
|
|
1726
|
+
return sessions.size;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/git-context.ts
|
|
1730
|
+
import { execFile } from "child_process";
|
|
1731
|
+
import { promisify } from "util";
|
|
1732
|
+
var execFileAsync = promisify(execFile);
|
|
1733
|
+
var GIT_TIMEOUT_MS = 2e3;
|
|
1734
|
+
async function runGit(args, cwd) {
|
|
1735
|
+
try {
|
|
1736
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
1737
|
+
cwd,
|
|
1738
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1739
|
+
windowsHide: true
|
|
1740
|
+
});
|
|
1741
|
+
const trimmed = stdout.trim();
|
|
1742
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1743
|
+
} catch {
|
|
1744
|
+
return void 0;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function sanitizeRemote(raw) {
|
|
1748
|
+
if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
|
|
1749
|
+
return raw;
|
|
1750
|
+
}
|
|
1751
|
+
try {
|
|
1752
|
+
const url = new URL(raw);
|
|
1753
|
+
url.username = "";
|
|
1754
|
+
url.password = "";
|
|
1755
|
+
return url.toString();
|
|
1756
|
+
} catch {
|
|
1757
|
+
return raw;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function collectGitContext(cwd = process.cwd()) {
|
|
1761
|
+
const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
1762
|
+
if (insideRepo !== "true") return {};
|
|
1763
|
+
const [remote, branch, sha] = await Promise.all([
|
|
1764
|
+
runGit(["remote", "get-url", "origin"], cwd),
|
|
1765
|
+
runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
|
|
1766
|
+
runGit(["rev-parse", "HEAD"], cwd)
|
|
1767
|
+
]);
|
|
1768
|
+
const ctx = {};
|
|
1769
|
+
if (remote) ctx.git_remote = sanitizeRemote(remote);
|
|
1770
|
+
if (branch && branch !== "HEAD") ctx.git_branch = branch;
|
|
1771
|
+
if (sha) ctx.git_commit_sha = sha;
|
|
1772
|
+
return ctx;
|
|
1773
|
+
}
|
|
1774
|
+
var CACHE_TTL_MS = 3e4;
|
|
1775
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1776
|
+
async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
|
|
1777
|
+
const hit = cache.get(cwd);
|
|
1778
|
+
if (hit && hit.expiresAt > now) return hit.value;
|
|
1779
|
+
const value = await collectGitContext(cwd);
|
|
1780
|
+
cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
|
|
1781
|
+
return value;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/commands/login.ts
|
|
1785
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
|
|
1786
|
+
import { join as join4 } from "path";
|
|
1787
|
+
import { homedir as homedir2 } from "os";
|
|
1788
|
+
import readline from "readline/promises";
|
|
1789
|
+
import chalk from "chalk";
|
|
1790
|
+
var CONFIG_DIR_NAME2 = ".pretense";
|
|
1791
|
+
var CONFIG_FILE2 = "config.json";
|
|
1792
|
+
function getUserDashboardConfigDir() {
|
|
1793
|
+
return join4(homedir2(), CONFIG_DIR_NAME2);
|
|
1794
|
+
}
|
|
1795
|
+
function getUserDashboardConfigPath() {
|
|
1796
|
+
return join4(getUserDashboardConfigDir(), CONFIG_FILE2);
|
|
1797
|
+
}
|
|
1798
|
+
function isValidKeyShape(key) {
|
|
1799
|
+
const trimmed = key.trim();
|
|
1800
|
+
if (trimmed.length < 24) return false;
|
|
1801
|
+
if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
|
|
1802
|
+
return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
|
|
1803
|
+
}
|
|
1804
|
+
function readStdinIfPiped() {
|
|
1805
|
+
if (process.stdin.isTTY) return "";
|
|
1806
|
+
try {
|
|
1807
|
+
const chunks = [];
|
|
1808
|
+
const buf = Buffer.alloc(4096);
|
|
1809
|
+
let bytesRead = 0;
|
|
1810
|
+
do {
|
|
1811
|
+
try {
|
|
1812
|
+
bytesRead = readSync(0, buf, 0, buf.length, null);
|
|
1813
|
+
} catch {
|
|
1814
|
+
bytesRead = 0;
|
|
1815
|
+
}
|
|
1816
|
+
if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
1817
|
+
} while (bytesRead === buf.length);
|
|
1818
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1819
|
+
} catch {
|
|
1820
|
+
return "";
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function maskKey(key) {
|
|
1824
|
+
const trimmed = key.trim();
|
|
1825
|
+
if (trimmed.length <= 12) return trimmed;
|
|
1826
|
+
return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
|
|
1827
|
+
}
|
|
1828
|
+
function logDashboardCredentialHint() {
|
|
1829
|
+
const env = process.env["PRETENSE_API_KEY"]?.trim() ?? "";
|
|
1830
|
+
const envOk = env.length > 0 && isValidKeyShape(env);
|
|
1831
|
+
const abs = getUserDashboardConfigPath();
|
|
1832
|
+
const fk = loadApiKey();
|
|
1833
|
+
if (envOk) {
|
|
1834
|
+
console.error(
|
|
1835
|
+
chalk.gray(
|
|
1836
|
+
`[pretense] Dashboard key source: PRETENSE_API_KEY in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
|
|
1837
|
+
)
|
|
1838
|
+
);
|
|
1839
|
+
} else if (fk && isValidKeyShape(fk)) {
|
|
1840
|
+
console.error(chalk.gray(`[pretense] Dashboard key source: ${abs} (${maskKey(fk)})`));
|
|
1841
|
+
} else {
|
|
1842
|
+
console.error(chalk.gray("[pretense] Dashboard key source: none configured."));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
|
|
1846
|
+
if (!existsSync5(configDir)) {
|
|
1847
|
+
mkdirSync5(configDir, { recursive: true });
|
|
1848
|
+
}
|
|
1849
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1850
|
+
let existing = {};
|
|
1851
|
+
if (existsSync5(path)) {
|
|
1852
|
+
try {
|
|
1853
|
+
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1854
|
+
} catch {
|
|
1855
|
+
existing = {};
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const next = {
|
|
1859
|
+
...existing,
|
|
1860
|
+
api_key: key.trim(),
|
|
1861
|
+
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1862
|
+
};
|
|
1863
|
+
writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1864
|
+
try {
|
|
1865
|
+
chmodSync(path, 384);
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
return path;
|
|
1869
|
+
}
|
|
1870
|
+
function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
|
|
1871
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1872
|
+
if (!existsSync5(path)) return { removed: false, path };
|
|
1873
|
+
let existing = {};
|
|
1874
|
+
try {
|
|
1875
|
+
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1876
|
+
} catch {
|
|
1877
|
+
return { removed: false, path };
|
|
1878
|
+
}
|
|
1879
|
+
const ak = existing.api_key;
|
|
1880
|
+
const hadKey = typeof ak === "string" && ak.trim().length > 0;
|
|
1881
|
+
if (!hadKey) return { removed: false, path };
|
|
1882
|
+
delete existing.api_key;
|
|
1883
|
+
delete existing.saved_at;
|
|
1884
|
+
writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1885
|
+
try {
|
|
1886
|
+
chmodSync(path, 384);
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
return { removed: true, path };
|
|
1890
|
+
}
|
|
1891
|
+
function loadApiKey(configDir = getUserDashboardConfigDir()) {
|
|
1892
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1893
|
+
if (!existsSync5(path)) return null;
|
|
1894
|
+
try {
|
|
1895
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1896
|
+
const parsed = JSON.parse(raw);
|
|
1897
|
+
if (typeof parsed.api_key === "string") {
|
|
1898
|
+
const t = parsed.api_key.trim();
|
|
1899
|
+
if (t.length > 0) return t;
|
|
1900
|
+
}
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
function resolveDashboardKeyCandidate() {
|
|
1906
|
+
const env = (process.env["PRETENSE_API_KEY"] ?? "").trim();
|
|
1907
|
+
if (env.length > 0) {
|
|
1908
|
+
if (!isValidKeyShape(env)) {
|
|
1909
|
+
return { key: null, fromFile: false, badEnv: true };
|
|
1910
|
+
}
|
|
1911
|
+
return { key: env, fromFile: false, badEnv: false };
|
|
1912
|
+
}
|
|
1913
|
+
const fileKey = loadApiKey();
|
|
1914
|
+
if (fileKey && isValidKeyShape(fileKey)) {
|
|
1915
|
+
return { key: fileKey.trim(), fromFile: true, badEnv: false };
|
|
1916
|
+
}
|
|
1917
|
+
return { key: null, fromFile: Boolean(fileKey), badEnv: false };
|
|
1918
|
+
}
|
|
1919
|
+
function installDashboardKeyForCurrentProcess(key) {
|
|
1920
|
+
const trimmed = key.trim();
|
|
1921
|
+
const path = saveApiKey(trimmed);
|
|
1922
|
+
process.env["PRETENSE_API_KEY"] = trimmed;
|
|
1923
|
+
return path;
|
|
1924
|
+
}
|
|
1925
|
+
async function promptDashboardApiKeyAfterFailure() {
|
|
1926
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
1927
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1928
|
+
try {
|
|
1929
|
+
const line = (await rl.question(chalk.bold("Pretense dashboard API key: "))).trim();
|
|
1930
|
+
return line.length > 0 ? line : null;
|
|
1931
|
+
} finally {
|
|
1932
|
+
rl.close();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
async function ensureDashboardKeyForStart() {
|
|
1936
|
+
const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
|
|
1937
|
+
if (badEnv) {
|
|
1938
|
+
console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
|
|
1939
|
+
console.error(
|
|
1940
|
+
chalk.gray("Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretense/config.json.")
|
|
1941
|
+
);
|
|
1942
|
+
process.exit(1);
|
|
1943
|
+
}
|
|
1944
|
+
if (key) {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (fromFile) {
|
|
1948
|
+
console.warn(
|
|
1949
|
+
chalk.yellow(
|
|
1950
|
+
"Ignoring invalid `api_key` in ~/.pretense/config.json (wrong shape). Enter a new key or run `pretense login --key ...`."
|
|
1951
|
+
)
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const stdinOk = process.stdin.isTTY;
|
|
1955
|
+
const stdoutOk = process.stdout.isTTY;
|
|
1956
|
+
if (!stdinOk || !stdoutOk) {
|
|
1957
|
+
console.error(chalk.red("Error: Pretense API key required to start the proxy."));
|
|
1958
|
+
console.error(
|
|
1959
|
+
chalk.gray(
|
|
1960
|
+
"Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
1961
|
+
)
|
|
1962
|
+
);
|
|
1963
|
+
process.exit(1);
|
|
1964
|
+
}
|
|
1965
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1966
|
+
try {
|
|
1967
|
+
console.log(chalk.cyan("No Pretense API key found in ~/.pretense/config.json or $PRETENSE_API_KEY."));
|
|
1968
|
+
const entered = (await rl.question(chalk.bold("Enter your Pretense API key: "))).trim();
|
|
1969
|
+
if (!entered) {
|
|
1970
|
+
console.error(chalk.red("No key entered; exiting."));
|
|
1971
|
+
process.exit(1);
|
|
1972
|
+
}
|
|
1973
|
+
if (!isValidKeyShape(entered)) {
|
|
1974
|
+
console.error(chalk.red("That does not look like a Pretense API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
|
|
1975
|
+
console.error(chalk.gray("Get one from your Pretense dashboard, then run `pretense login --key ...`."));
|
|
1976
|
+
process.exit(1);
|
|
1977
|
+
}
|
|
1978
|
+
const path = installDashboardKeyForCurrentProcess(entered);
|
|
1979
|
+
console.log(chalk.green("Saved API key to"), chalk.bold(path));
|
|
1980
|
+
console.log(chalk.gray(` ${maskKey(entered)}`));
|
|
1981
|
+
} finally {
|
|
1982
|
+
rl.close();
|
|
1983
|
+
}
|
|
1984
|
+
const persisted = resolveDashboardKeyCandidate();
|
|
1985
|
+
if (!persisted.key || persisted.badEnv) {
|
|
1986
|
+
console.error(chalk.red("Error: Pretense dashboard API key could not be confirmed after login prompt."));
|
|
1987
|
+
process.exit(1);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function runLogin(opts = {}) {
|
|
1991
|
+
const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
|
|
1992
|
+
if (!candidate) {
|
|
1993
|
+
console.error(chalk.red("Error: no API key provided."));
|
|
1994
|
+
console.error(
|
|
1995
|
+
chalk.gray("Try: pretense login --key pk_live_xxxxx")
|
|
1996
|
+
);
|
|
1997
|
+
console.error(
|
|
1998
|
+
chalk.gray(" or: echo $PRETENSE_API_KEY | pretense login")
|
|
1999
|
+
);
|
|
2000
|
+
return 1;
|
|
2001
|
+
}
|
|
2002
|
+
if (!isValidKeyShape(candidate)) {
|
|
2003
|
+
console.error(chalk.red("Error: that does not look like a Pretense API key."));
|
|
2004
|
+
console.error(
|
|
2005
|
+
chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
|
|
2006
|
+
);
|
|
2007
|
+
console.error(
|
|
2008
|
+
chalk.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
|
|
2009
|
+
);
|
|
2010
|
+
return 1;
|
|
2011
|
+
}
|
|
2012
|
+
const path = saveApiKey(candidate, opts.configDir);
|
|
2013
|
+
console.log(chalk.green("Saved API key to"), chalk.bold(path));
|
|
2014
|
+
console.log(chalk.gray(` ${maskKey(candidate)}`));
|
|
2015
|
+
console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
|
|
2016
|
+
return 0;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/backend-client.ts
|
|
2020
|
+
function usageSummaryFromValidate(v) {
|
|
2021
|
+
return {
|
|
2022
|
+
plan: v.plan,
|
|
2023
|
+
monthlyMutationLimit: v.monthlyMutationLimit,
|
|
2024
|
+
mutationsUsed: v.mutationsUsed,
|
|
2025
|
+
mutationsRemaining: v.mutationsRemaining,
|
|
2026
|
+
requestsThisPeriod: 0,
|
|
2027
|
+
secretsBlockedThisPeriod: 0,
|
|
2028
|
+
periodResetsAt: v.periodResetsAt,
|
|
2029
|
+
keyExpiresAt: null,
|
|
2030
|
+
keyStatus: "active"
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
|
|
2034
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
2035
|
+
function getConfiguredApiRoot(rootOverride) {
|
|
2036
|
+
const raw = (rootOverride ?? process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
|
|
2037
|
+
if (!raw) {
|
|
2038
|
+
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2039
|
+
}
|
|
2040
|
+
let normalized = raw;
|
|
2041
|
+
if (!/^https?:\/\//i.test(normalized)) {
|
|
2042
|
+
normalized = `https://${normalized}`;
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
const u = new URL(normalized);
|
|
2046
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? "https://api.pretense.ai" : u.origin;
|
|
2047
|
+
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
2048
|
+
return `${origin}${path}`;
|
|
2049
|
+
} catch {
|
|
2050
|
+
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
function getCliApiUrl(pathWithinCli, rootOverride) {
|
|
2054
|
+
const root = getConfiguredApiRoot(rootOverride);
|
|
2055
|
+
const tail = pathWithinCli.replace(/^\/+/, "");
|
|
2056
|
+
return `${root}/api/cli/${tail}`;
|
|
2057
|
+
}
|
|
2058
|
+
function getApiRequestTimeoutMs() {
|
|
2059
|
+
const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2060
|
+
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2061
|
+
const n = parseInt(raw, 10);
|
|
2062
|
+
return Math.min(Math.max(n, 3e3), 12e4);
|
|
2063
|
+
}
|
|
2064
|
+
function getApiKey() {
|
|
2065
|
+
const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
|
|
2066
|
+
if (fromEnv) return fromEnv;
|
|
2067
|
+
const fromFile = loadApiKey();
|
|
2068
|
+
if (!fromFile) return null;
|
|
2069
|
+
const t = fromFile.trim();
|
|
2070
|
+
return t.length > 0 ? t : null;
|
|
2071
|
+
}
|
|
2072
|
+
function getPretenseApiKey() {
|
|
2073
|
+
return getApiKey();
|
|
2074
|
+
}
|
|
2075
|
+
var cachedValidation = null;
|
|
2076
|
+
var lastValidationSuccessAt = 0;
|
|
2077
|
+
var VALIDATION_CACHE_FRESH_MS = 45e3;
|
|
2078
|
+
function getCachedValidation() {
|
|
2079
|
+
return cachedValidation;
|
|
2080
|
+
}
|
|
2081
|
+
function clearValidationCache() {
|
|
2082
|
+
cachedValidation = null;
|
|
2083
|
+
lastValidationSuccessAt = 0;
|
|
2084
|
+
}
|
|
2085
|
+
function bumpCachedUsageAfterLog(identifiersMutated) {
|
|
2086
|
+
if (!cachedValidation || identifiersMutated <= 0) return;
|
|
2087
|
+
cachedValidation.mutationsUsed += identifiersMutated;
|
|
2088
|
+
if (cachedValidation.mutationsRemaining !== -1) {
|
|
2089
|
+
cachedValidation.mutationsRemaining = Math.max(
|
|
2090
|
+
0,
|
|
2091
|
+
cachedValidation.mutationsRemaining - identifiersMutated
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function makeBackendReachabilityError(code, message) {
|
|
2096
|
+
const err = new Error(message);
|
|
2097
|
+
err.code = code;
|
|
2098
|
+
return err;
|
|
2099
|
+
}
|
|
2100
|
+
async function validateKey(opts = {}) {
|
|
2101
|
+
const enforceReachability = opts.enforceReachability === true;
|
|
2102
|
+
const now = Date.now();
|
|
2103
|
+
const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
|
|
2104
|
+
if (!opts.force && cacheFresh) {
|
|
2105
|
+
return cachedValidation;
|
|
2106
|
+
}
|
|
2107
|
+
const validateUrl = getCliApiUrl("auth/validate");
|
|
2108
|
+
const timeoutMs = getApiRequestTimeoutMs();
|
|
2109
|
+
const apiKey = getApiKey();
|
|
2110
|
+
if (!apiKey) {
|
|
2111
|
+
if (enforceReachability) {
|
|
2112
|
+
const err = new Error("Missing Pretense dashboard API key");
|
|
2113
|
+
err.code = "MISSING_API_KEY";
|
|
2114
|
+
cachedValidation = null;
|
|
2115
|
+
lastValidationSuccessAt = 0;
|
|
2116
|
+
throw err;
|
|
2117
|
+
}
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
const controller = new AbortController();
|
|
2121
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2122
|
+
try {
|
|
2123
|
+
const resp = await fetch(validateUrl, {
|
|
2124
|
+
method: "POST",
|
|
2125
|
+
headers: {
|
|
2126
|
+
"content-type": "application/json",
|
|
2127
|
+
authorization: `Bearer ${apiKey}`
|
|
2128
|
+
},
|
|
2129
|
+
body: "{}",
|
|
2130
|
+
signal: controller.signal
|
|
2131
|
+
});
|
|
2132
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
2133
|
+
const raw = await resp.json().catch(() => ({}));
|
|
2134
|
+
const msg = typeof raw.message === "string" ? raw.message : "API key rejected by server";
|
|
2135
|
+
const err = new Error(msg);
|
|
2136
|
+
err.code = typeof raw.code === "string" ? raw.code : "INVALID_API_KEY";
|
|
2137
|
+
cachedValidation = null;
|
|
2138
|
+
lastValidationSuccessAt = 0;
|
|
2139
|
+
throw err;
|
|
2140
|
+
}
|
|
2141
|
+
if (!resp.ok) {
|
|
2142
|
+
const msg = `Pretense API returned HTTP ${resp.status} from ${validateUrl}`;
|
|
2143
|
+
if (enforceReachability) {
|
|
2144
|
+
cachedValidation = null;
|
|
2145
|
+
lastValidationSuccessAt = 0;
|
|
2146
|
+
throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
|
|
2147
|
+
}
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
let data;
|
|
2151
|
+
try {
|
|
2152
|
+
data = await resp.json();
|
|
2153
|
+
} catch {
|
|
2154
|
+
if (enforceReachability) {
|
|
2155
|
+
cachedValidation = null;
|
|
2156
|
+
lastValidationSuccessAt = 0;
|
|
2157
|
+
throw makeBackendReachabilityError(
|
|
2158
|
+
"AUTH_BACKEND_REJECTED",
|
|
2159
|
+
`Invalid JSON from ${validateUrl}`
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
if (typeof data.valid === "boolean" && data.valid === false) {
|
|
2165
|
+
const err = new Error("API key is not valid");
|
|
2166
|
+
err.code = "INVALID_API_KEY";
|
|
2167
|
+
cachedValidation = null;
|
|
2168
|
+
lastValidationSuccessAt = 0;
|
|
2169
|
+
throw err;
|
|
2170
|
+
}
|
|
2171
|
+
cachedValidation = data;
|
|
2172
|
+
lastValidationSuccessAt = Date.now();
|
|
2173
|
+
return data;
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
if (err.code === "KEY_REVOKED" || err.code === "KEY_EXPIRED" || err.code === "INVALID_API_KEY" || err.code === "AUTH_ERROR" || err.code === "ORG_INACTIVE" || err.code === "MISSING_API_KEY" || err.code === "BACKEND_UNAVAILABLE" || err.code === "AUTH_BACKEND_REJECTED") {
|
|
2176
|
+
throw err;
|
|
2177
|
+
}
|
|
2178
|
+
if (enforceReachability) {
|
|
2179
|
+
const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach Pretense API at ${validateUrl}: ${err.message}`;
|
|
2180
|
+
cachedValidation = null;
|
|
2181
|
+
lastValidationSuccessAt = 0;
|
|
2182
|
+
throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
|
|
2183
|
+
}
|
|
2184
|
+
if (opts.verbose) {
|
|
2185
|
+
process.stderr.write(
|
|
2186
|
+
`[PRETENSE] Backend validation failed: ${err.message}
|
|
2187
|
+
`
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
} finally {
|
|
2192
|
+
clearTimeout(timer);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
async function fetchUsageSummary(opts = {}) {
|
|
2196
|
+
const summaryUrl = getCliApiUrl("usage/summary");
|
|
2197
|
+
const apiKey = getApiKey();
|
|
2198
|
+
if (!apiKey) return null;
|
|
2199
|
+
const controller = new AbortController();
|
|
2200
|
+
const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
|
|
2201
|
+
try {
|
|
2202
|
+
const resp = await fetch(summaryUrl, {
|
|
2203
|
+
method: "GET",
|
|
2204
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
2205
|
+
signal: controller.signal
|
|
2206
|
+
});
|
|
2207
|
+
if (!resp.ok) return null;
|
|
2208
|
+
return await resp.json();
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
if (opts.verbose) {
|
|
2211
|
+
process.stderr.write(
|
|
2212
|
+
`[PRETENSE] Usage summary fetch failed: ${err.message}
|
|
2213
|
+
`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
return null;
|
|
2217
|
+
} finally {
|
|
2218
|
+
clearTimeout(timer);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
async function isWithinLimits() {
|
|
2222
|
+
const v = getCachedValidation();
|
|
2223
|
+
if (!v) return { allowed: true };
|
|
2224
|
+
if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
|
|
2225
|
+
return {
|
|
2226
|
+
allowed: false,
|
|
2227
|
+
reason: `Monthly mutation limit reached (${v.mutationsUsed}/${v.monthlyMutationLimit}). Upgrade at https://pretense.ai/pricing`
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
return { allowed: true };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/log-uploader.ts
|
|
2234
|
+
async function uploadLog(event, options) {
|
|
2235
|
+
const apiRoot = options.apiUrl?.trim() || void 0;
|
|
2236
|
+
const endpoint = getCliApiUrl("log", apiRoot);
|
|
2237
|
+
const { apiKey, verbose = false, timeoutMs = getApiRequestTimeoutMs() } = options;
|
|
2238
|
+
if (!apiKey) return;
|
|
2239
|
+
const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
|
|
2240
|
+
const body = {
|
|
2241
|
+
...event,
|
|
2242
|
+
...gitCtx,
|
|
2243
|
+
repo_remote_url: gitCtx.git_remote,
|
|
2244
|
+
git_branch: gitCtx.git_branch,
|
|
2245
|
+
commit_sha: gitCtx.git_commit_sha
|
|
2246
|
+
};
|
|
2247
|
+
const controller = new AbortController();
|
|
2248
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2249
|
+
try {
|
|
2250
|
+
const resp = await fetch(endpoint, {
|
|
2251
|
+
method: "POST",
|
|
2252
|
+
headers: {
|
|
2253
|
+
"content-type": "application/json",
|
|
2254
|
+
authorization: `Bearer ${apiKey}`
|
|
2255
|
+
},
|
|
2256
|
+
body: JSON.stringify(body),
|
|
2257
|
+
signal: controller.signal
|
|
2258
|
+
});
|
|
2259
|
+
if (!resp.ok && verbose) {
|
|
2260
|
+
process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status}
|
|
2261
|
+
`);
|
|
2262
|
+
}
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
if (verbose) {
|
|
2265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2266
|
+
process.stderr.write(`[PRETENSE] log upload failed: ${msg}
|
|
2267
|
+
`);
|
|
2268
|
+
}
|
|
2269
|
+
} finally {
|
|
2270
|
+
clearTimeout(timer);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/start-client-instructions.ts
|
|
2275
|
+
import chalk2 from "chalk";
|
|
2276
|
+
function printStartClientInstructions(port) {
|
|
2277
|
+
const host = `http://localhost:${port}`;
|
|
2278
|
+
console.log();
|
|
2279
|
+
console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
|
|
2280
|
+
console.log(chalk2.gray("Point the CLI at the Pretense backend API (optional if using default):"));
|
|
2281
|
+
console.log(
|
|
2282
|
+
chalk2.white(`export PRETENSE_API_URL='https://api.pretense.ai'`)
|
|
2283
|
+
);
|
|
2284
|
+
console.log();
|
|
2285
|
+
console.log(chalk2.gray("Log in with your Pretense dashboard API key:"));
|
|
2286
|
+
console.log(chalk2.white("pretense login --key your-pretense-api-key"));
|
|
2287
|
+
console.log();
|
|
2288
|
+
console.log(
|
|
2289
|
+
chalk2.gray("Set the LLM base URL for your provider (proxy below):")
|
|
2290
|
+
);
|
|
2291
|
+
console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
|
|
2292
|
+
console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
|
|
2293
|
+
console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
|
|
2294
|
+
console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
|
|
2295
|
+
console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
|
|
2296
|
+
console.log();
|
|
2297
|
+
console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
|
|
2298
|
+
console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
|
|
2299
|
+
console.log();
|
|
2300
|
+
console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
|
|
2301
|
+
console.log(
|
|
2302
|
+
chalk2.gray(
|
|
2303
|
+
"Stop the proxy: press Ctrl+C in the terminal where pretense start is running."
|
|
2304
|
+
)
|
|
2305
|
+
);
|
|
2306
|
+
console.log(
|
|
2307
|
+
chalk2.gray(
|
|
2308
|
+
"Remove the saved Pretense dashboard key from ~/.pretense/config.json:"
|
|
2309
|
+
)
|
|
2310
|
+
);
|
|
2311
|
+
console.log(chalk2.white("pretense logout"));
|
|
2312
|
+
console.log(
|
|
2313
|
+
chalk2.gray(
|
|
2314
|
+
"(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
|
|
2315
|
+
)
|
|
2316
|
+
);
|
|
2317
|
+
console.log(
|
|
2318
|
+
chalk2.gray(
|
|
2319
|
+
"PRETENSE_API_KEY set in that same terminal \u2014 then press Ctrl+C and run pretense start again."
|
|
2320
|
+
)
|
|
2321
|
+
);
|
|
2322
|
+
console.log();
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/version.ts
|
|
2326
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2327
|
+
import { fileURLToPath } from "url";
|
|
2328
|
+
var cached = null;
|
|
2329
|
+
function getCliSemver() {
|
|
2330
|
+
if (cached !== null) return cached;
|
|
2331
|
+
try {
|
|
2332
|
+
const url = new URL("../package.json", import.meta.url);
|
|
2333
|
+
const raw = readFileSync6(fileURLToPath(url), "utf-8");
|
|
2334
|
+
cached = JSON.parse(raw).version ?? "0.0.0";
|
|
2335
|
+
} catch {
|
|
2336
|
+
cached = "0.0.0";
|
|
2337
|
+
}
|
|
2338
|
+
return cached;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/proxy.ts
|
|
2342
|
+
var dashboardSavedCredentialsRevoked = false;
|
|
2343
|
+
function attachDashboardLogoutWatcher(enabled) {
|
|
2344
|
+
if (!enabled) return;
|
|
2345
|
+
const dir = getUserDashboardConfigDir();
|
|
2346
|
+
let debounceTimer;
|
|
2347
|
+
const applyFilesystemCredentialState = () => {
|
|
2348
|
+
const fk = loadApiKey();
|
|
2349
|
+
if (!fk) {
|
|
2350
|
+
if (dashboardSavedCredentialsRevoked) return;
|
|
2351
|
+
dashboardSavedCredentialsRevoked = true;
|
|
2352
|
+
delete process.env["PRETENSE_API_KEY"];
|
|
2353
|
+
clearValidationCache();
|
|
2354
|
+
try {
|
|
2355
|
+
process.stderr.write(
|
|
2356
|
+
`\x1B[33m[PRETENSE] Saved dashboard API key removed (~/.pretense/config.json, e.g. pretense logout). LLM proxy POST requests are disabled until you run pretense login and restart pretense start.\x1B[0m
|
|
2357
|
+
`
|
|
2358
|
+
);
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (dashboardSavedCredentialsRevoked) {
|
|
2364
|
+
dashboardSavedCredentialsRevoked = false;
|
|
2365
|
+
clearValidationCache();
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
const schedule = () => {
|
|
2369
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2370
|
+
debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
|
|
2371
|
+
};
|
|
2372
|
+
try {
|
|
2373
|
+
if (!existsSync6(dir)) return;
|
|
2374
|
+
watch(dir, () => {
|
|
2375
|
+
schedule();
|
|
2376
|
+
});
|
|
2377
|
+
} catch {
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
function anthropicUpstream() {
|
|
2381
|
+
return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
|
|
2382
|
+
}
|
|
2383
|
+
function openaiUpstream() {
|
|
2384
|
+
return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
|
|
2385
|
+
}
|
|
2386
|
+
function geminiUpstream() {
|
|
2387
|
+
return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
|
|
2388
|
+
}
|
|
2389
|
+
function ollamaUpstream() {
|
|
2390
|
+
return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
|
|
2391
|
+
}
|
|
2392
|
+
function detectUpstream(path) {
|
|
2393
|
+
if (path.startsWith("/v1/messages")) return anthropicUpstream();
|
|
2394
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
2395
|
+
return openaiUpstream();
|
|
2396
|
+
}
|
|
2397
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
|
|
2398
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
|
|
2399
|
+
return anthropicUpstream();
|
|
2400
|
+
}
|
|
2401
|
+
function detectProvider(path) {
|
|
2402
|
+
if (path.startsWith("/v1/messages")) return "anthropic";
|
|
2403
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
2404
|
+
return "openai";
|
|
2405
|
+
}
|
|
2406
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
|
|
2407
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
|
|
2408
|
+
return "anthropic";
|
|
2409
|
+
}
|
|
2410
|
+
function stripPretenseOnlyForwardingHeaders(headers) {
|
|
2411
|
+
const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
|
|
2412
|
+
for (const k of [...Object.keys(headers)]) {
|
|
2413
|
+
if (drop.has(k.toLowerCase())) delete headers[k];
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
1110
2416
|
function extractText(body) {
|
|
1111
2417
|
const parts = [];
|
|
1112
2418
|
if (typeof body["system"] === "string") parts.push(body["system"]);
|
|
@@ -1184,11 +2490,29 @@ function createProxy(opts = {}) {
|
|
|
1184
2490
|
const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
|
|
1185
2491
|
const verbose = opts.verbose ?? false;
|
|
1186
2492
|
const app = new Hono();
|
|
2493
|
+
app.use("*", requireApiKey);
|
|
2494
|
+
app.get(
|
|
2495
|
+
"/",
|
|
2496
|
+
(c) => c.json({
|
|
2497
|
+
name: "pretense",
|
|
2498
|
+
version: getCliSemver(),
|
|
2499
|
+
status: "running",
|
|
2500
|
+
routes: {
|
|
2501
|
+
"GET /": "This welcome page",
|
|
2502
|
+
"GET /health": "Health check with uptime",
|
|
2503
|
+
"GET /stats": "Mutation statistics",
|
|
2504
|
+
"GET /audit": "Audit log (?limit=50)",
|
|
2505
|
+
"POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
|
|
2506
|
+
},
|
|
2507
|
+
languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
|
|
2508
|
+
docs: "https://pretense.ai/docs"
|
|
2509
|
+
})
|
|
2510
|
+
);
|
|
1187
2511
|
app.get(
|
|
1188
2512
|
"/health",
|
|
1189
2513
|
(c) => c.json({
|
|
1190
2514
|
status: "ok",
|
|
1191
|
-
version:
|
|
2515
|
+
version: getCliSemver(),
|
|
1192
2516
|
uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
|
|
1193
2517
|
stats: {
|
|
1194
2518
|
requestsProcessed: stats.requestsProcessed,
|
|
@@ -1200,7 +2524,8 @@ function createProxy(opts = {}) {
|
|
|
1200
2524
|
"/stats",
|
|
1201
2525
|
(c) => c.json({
|
|
1202
2526
|
...stats,
|
|
1203
|
-
storeSize: store.size
|
|
2527
|
+
storeSize: store.size,
|
|
2528
|
+
activeSessions: sessionCount()
|
|
1204
2529
|
})
|
|
1205
2530
|
);
|
|
1206
2531
|
app.get("/audit", (c) => {
|
|
@@ -1217,6 +2542,7 @@ function createProxy(opts = {}) {
|
|
|
1217
2542
|
});
|
|
1218
2543
|
delete headers2["host"];
|
|
1219
2544
|
headers2["host"] = new URL(upstream2).host;
|
|
2545
|
+
stripPretenseOnlyForwardingHeaders(headers2);
|
|
1220
2546
|
const resp = await fetch(url, { method: c.req.method, headers: headers2 });
|
|
1221
2547
|
return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
|
|
1222
2548
|
}
|
|
@@ -1233,20 +2559,106 @@ function createProxy(opts = {}) {
|
|
|
1233
2559
|
const requestId = nanoid(12);
|
|
1234
2560
|
const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1235
2561
|
const provider = detectProvider(c.req.path);
|
|
1236
|
-
|
|
1237
|
-
|
|
2562
|
+
if (dashboardSavedCredentialsRevoked) {
|
|
2563
|
+
return c.json(
|
|
2564
|
+
{
|
|
2565
|
+
error: {
|
|
2566
|
+
type: "pretense_session_logged_out",
|
|
2567
|
+
message: "Saved Pretense dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with PRETENSE_API_KEY in this shell, stop it (Ctrl+C) and start again after logout."
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
401
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
const pretenseDashboardKey = getPretenseApiKey();
|
|
2574
|
+
if (!pretenseDashboardKey) {
|
|
2575
|
+
return c.json(
|
|
2576
|
+
{
|
|
2577
|
+
error: {
|
|
2578
|
+
type: "pretense_dashboard_key_required",
|
|
2579
|
+
message: "Configure your Pretense dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
2580
|
+
}
|
|
2581
|
+
},
|
|
2582
|
+
401
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
try {
|
|
2586
|
+
await validateKey({ verbose, force: false, enforceReachability: true });
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
const code = err.code ?? "";
|
|
2589
|
+
const msg = err.message;
|
|
2590
|
+
const knownAuthCodes = /* @__PURE__ */ new Set([
|
|
2591
|
+
"KEY_REVOKED",
|
|
2592
|
+
"KEY_EXPIRED",
|
|
2593
|
+
"INVALID_API_KEY",
|
|
2594
|
+
"MISSING_API_KEY"
|
|
2595
|
+
]);
|
|
2596
|
+
if (knownAuthCodes.has(code)) {
|
|
2597
|
+
return c.json(
|
|
2598
|
+
{ error: { type: "pretense_auth_error", code, message: msg } },
|
|
2599
|
+
401
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
2603
|
+
return c.json(
|
|
2604
|
+
{ error: { type: "pretense_auth_backend_error", code, message: msg } },
|
|
2605
|
+
503
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
if (code === "ORG_INACTIVE") {
|
|
2609
|
+
return c.json(
|
|
2610
|
+
{ error: { type: "pretense_org_inactive", code, message: msg } },
|
|
2611
|
+
403
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
return c.json(
|
|
2615
|
+
{ error: { type: "pretense_auth_error", message: msg } },
|
|
2616
|
+
401
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
const sessHash = c.get("sessionHash");
|
|
2620
|
+
const session = getSession(sessHash);
|
|
2621
|
+
const tier = tierFromDashboardPlan(
|
|
2622
|
+
getCachedValidation()?.plan,
|
|
2623
|
+
detectTier()
|
|
2624
|
+
);
|
|
2625
|
+
const { monthlyMutations } = getTierLimits(tier);
|
|
1238
2626
|
const usage = loadUsage();
|
|
1239
|
-
if (tier === "free" && usage.mutations >=
|
|
2627
|
+
if (tier === "free" && usage.mutations >= monthlyMutations) {
|
|
1240
2628
|
return c.json(
|
|
1241
2629
|
{
|
|
1242
2630
|
error: {
|
|
1243
2631
|
type: "pretense_rate_limit",
|
|
1244
|
-
message: `
|
|
2632
|
+
message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
|
|
1245
2633
|
}
|
|
1246
2634
|
},
|
|
1247
2635
|
429
|
|
1248
2636
|
);
|
|
1249
2637
|
}
|
|
2638
|
+
const backendLimits = await isWithinLimits();
|
|
2639
|
+
if (!backendLimits.allowed) {
|
|
2640
|
+
return c.json(
|
|
2641
|
+
{
|
|
2642
|
+
error: {
|
|
2643
|
+
type: "pretense_rate_limit",
|
|
2644
|
+
message: backendLimits.reason ?? "Mutation limit exceeded."
|
|
2645
|
+
}
|
|
2646
|
+
},
|
|
2647
|
+
429
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const cliAuth = getCachedValidation();
|
|
2651
|
+
if (cliAuth?.permission === "read_only") {
|
|
2652
|
+
return c.json(
|
|
2653
|
+
{
|
|
2654
|
+
error: {
|
|
2655
|
+
type: "pretense_forbidden",
|
|
2656
|
+
message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretense proxy."
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
403
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
1250
2662
|
const fullText = extractText(body);
|
|
1251
2663
|
const secretScan = scan2(fullText);
|
|
1252
2664
|
const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
|
|
@@ -1270,11 +2682,25 @@ function createProxy(opts = {}) {
|
|
|
1270
2682
|
let tokensMutated = 0;
|
|
1271
2683
|
const mutatedBody = applyToBody(processedBody, (text) => {
|
|
1272
2684
|
const blocks = extractCodeBlocks(text);
|
|
1273
|
-
if (blocks.length === 0)
|
|
2685
|
+
if (blocks.length === 0) {
|
|
2686
|
+
const lang = detectLanguage(text);
|
|
2687
|
+
const result = mutate(text, lang);
|
|
2688
|
+
for (const [k, v] of result.map) {
|
|
2689
|
+
mutationMap.set(k, v);
|
|
2690
|
+
session.mutationMap.set(k, v);
|
|
2691
|
+
session.reverseMap.set(v, k);
|
|
2692
|
+
}
|
|
2693
|
+
tokensMutated += result.stats.tokensMutated;
|
|
2694
|
+
return result.mutatedCode;
|
|
2695
|
+
}
|
|
1274
2696
|
const mutatedBlocks = [];
|
|
1275
2697
|
for (const block of blocks) {
|
|
1276
2698
|
const result = mutate(block.code, block.language);
|
|
1277
|
-
for (const [k, v] of result.map
|
|
2699
|
+
for (const [k, v] of result.map) {
|
|
2700
|
+
mutationMap.set(k, v);
|
|
2701
|
+
session.mutationMap.set(k, v);
|
|
2702
|
+
session.reverseMap.set(v, k);
|
|
2703
|
+
}
|
|
1278
2704
|
tokensMutated += result.stats.tokensMutated;
|
|
1279
2705
|
mutatedBlocks.push(result.mutatedCode);
|
|
1280
2706
|
}
|
|
@@ -1282,8 +2708,23 @@ function createProxy(opts = {}) {
|
|
|
1282
2708
|
});
|
|
1283
2709
|
usage.mutations += tokensMutated;
|
|
1284
2710
|
saveUsage(usage);
|
|
1285
|
-
|
|
1286
|
-
|
|
2711
|
+
const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
|
|
2712
|
+
if (pretenseDashboardKey && !remoteLogDisabled) {
|
|
2713
|
+
void uploadLog(
|
|
2714
|
+
{
|
|
2715
|
+
file_count: 0,
|
|
2716
|
+
identifiers_mutated: tokensMutated,
|
|
2717
|
+
secrets_blocked: blockedSecrets.length,
|
|
2718
|
+
llm_provider: provider,
|
|
2719
|
+
cli_version: getCliSemver()
|
|
2720
|
+
},
|
|
2721
|
+
{ apiKey: pretenseDashboardKey, verbose }
|
|
2722
|
+
);
|
|
2723
|
+
bumpCachedUsageAfterLog(tokensMutated);
|
|
2724
|
+
}
|
|
2725
|
+
const warningThreshold = Math.floor(monthlyMutations * 0.8);
|
|
2726
|
+
if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
|
|
2727
|
+
printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
|
|
1287
2728
|
}
|
|
1288
2729
|
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
1289
2730
|
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
|
|
@@ -1314,6 +2755,7 @@ function createProxy(opts = {}) {
|
|
|
1314
2755
|
delete headers["content-length"];
|
|
1315
2756
|
headers["host"] = new URL(upstream).host;
|
|
1316
2757
|
headers["x-pretense-request-id"] = requestId;
|
|
2758
|
+
stripPretenseOnlyForwardingHeaders(headers);
|
|
1317
2759
|
const forwardBody = JSON.stringify(mutatedBody);
|
|
1318
2760
|
let upstreamResp;
|
|
1319
2761
|
try {
|
|
@@ -1326,14 +2768,59 @@ function createProxy(opts = {}) {
|
|
|
1326
2768
|
return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
|
|
1327
2769
|
}
|
|
1328
2770
|
if (body["stream"] === true) {
|
|
1329
|
-
|
|
2771
|
+
const streamReverseMap = new Map(mutationMap);
|
|
2772
|
+
const upstream2 = upstreamResp.body;
|
|
2773
|
+
if (!upstream2 || tokensMutated === 0) {
|
|
2774
|
+
return new Response(upstream2, {
|
|
2775
|
+
status: upstreamResp.status,
|
|
2776
|
+
headers: {
|
|
2777
|
+
"content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
|
|
2778
|
+
"cache-control": "no-cache",
|
|
2779
|
+
"x-pretense-request-id": requestId,
|
|
2780
|
+
"x-pretense-protected": "true",
|
|
2781
|
+
"x-pretense-mutations": String(tokensMutated)
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
const decoder = new TextDecoder();
|
|
2786
|
+
const encoder = new TextEncoder();
|
|
2787
|
+
let buffer = "";
|
|
2788
|
+
const transform = new TransformStream({
|
|
2789
|
+
transform(chunk, controller) {
|
|
2790
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
2791
|
+
const lines = buffer.split("\n");
|
|
2792
|
+
buffer = lines.pop() ?? "";
|
|
2793
|
+
for (const line of lines) {
|
|
2794
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
2795
|
+
try {
|
|
2796
|
+
const json = JSON.parse(line.slice(6));
|
|
2797
|
+
const reversed = applyToBody(json, (text) => reverse(text, streamReverseMap));
|
|
2798
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(reversed)}
|
|
2799
|
+
`));
|
|
2800
|
+
} catch {
|
|
2801
|
+
controller.enqueue(encoder.encode(line + "\n"));
|
|
2802
|
+
}
|
|
2803
|
+
} else {
|
|
2804
|
+
controller.enqueue(encoder.encode(line + "\n"));
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
flush(controller) {
|
|
2809
|
+
if (buffer.length > 0) {
|
|
2810
|
+
controller.enqueue(encoder.encode(buffer));
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
const reversedStream = upstream2.pipeThrough(transform);
|
|
2815
|
+
return new Response(reversedStream, {
|
|
1330
2816
|
status: upstreamResp.status,
|
|
1331
2817
|
headers: {
|
|
1332
2818
|
"content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
|
|
1333
2819
|
"cache-control": "no-cache",
|
|
1334
2820
|
"x-pretense-request-id": requestId,
|
|
1335
2821
|
"x-pretense-protected": "true",
|
|
1336
|
-
"x-pretense-mutations": String(tokensMutated)
|
|
2822
|
+
"x-pretense-mutations": String(tokensMutated),
|
|
2823
|
+
"x-pretense-stream-reversal": "active"
|
|
1337
2824
|
}
|
|
1338
2825
|
});
|
|
1339
2826
|
}
|
|
@@ -1363,31 +2850,559 @@ function createProxy(opts = {}) {
|
|
|
1363
2850
|
});
|
|
1364
2851
|
return app;
|
|
1365
2852
|
}
|
|
2853
|
+
function isPortAvailable(port) {
|
|
2854
|
+
return new Promise((resolve3) => {
|
|
2855
|
+
const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
|
|
2856
|
+
tester.once("close", () => resolve3(true)).close();
|
|
2857
|
+
}).listen(port, "127.0.0.1");
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
2861
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
2862
|
+
const candidate = startPort + i;
|
|
2863
|
+
if (candidate < 1 || candidate > 65535) break;
|
|
2864
|
+
if (await isPortAvailable(candidate)) return candidate;
|
|
2865
|
+
}
|
|
2866
|
+
throw new Error(
|
|
2867
|
+
`No available port found in range ${startPort}-${startPort + maxAttempts - 1}. Free a port or pass --port <number>.`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
1366
2870
|
function startProxy(opts = {}) {
|
|
1367
|
-
const
|
|
2871
|
+
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
1368
2872
|
const app = createProxy(opts);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
\
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
2873
|
+
void (async () => {
|
|
2874
|
+
const pretenseDashboardKeyFromEnvAtLaunch = !!process.env["PRETENSE_API_KEY"]?.trim();
|
|
2875
|
+
const dashboardKey = getPretenseApiKey();
|
|
2876
|
+
if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
|
|
2877
|
+
process.stderr.write(
|
|
2878
|
+
`\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
|
|
2879
|
+
\x1B[31m[PRETENSE] or set PRETENSE_API_KEY. Get a key at https://pretense.ai/dashboard/settings/api-keys\x1B[0m
|
|
2880
|
+
`
|
|
2881
|
+
);
|
|
2882
|
+
process.exit(1);
|
|
2883
|
+
}
|
|
2884
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2885
|
+
for (; ; ) {
|
|
2886
|
+
try {
|
|
2887
|
+
const validation = await validateKey({
|
|
2888
|
+
force: true,
|
|
2889
|
+
verbose: opts.verbose,
|
|
2890
|
+
enforceReachability: true
|
|
2891
|
+
});
|
|
2892
|
+
if (!validation) {
|
|
2893
|
+
process.stderr.write(
|
|
2894
|
+
`\x1B[31m[PRETENSE] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
|
|
2895
|
+
`
|
|
2896
|
+
);
|
|
2897
|
+
process.exit(1);
|
|
2898
|
+
}
|
|
2899
|
+
const plan = validation.plan ?? "FREE";
|
|
2900
|
+
const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
|
|
2901
|
+
process.stdout.write(
|
|
2902
|
+
`\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
|
|
2903
|
+
`
|
|
2904
|
+
);
|
|
2905
|
+
break;
|
|
2906
|
+
} catch (err) {
|
|
2907
|
+
const code = err.code;
|
|
2908
|
+
const msg = err.message;
|
|
2909
|
+
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
2910
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
1385
2911
|
`);
|
|
1386
|
-
|
|
2912
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL if using a custom API.\x1B[0m
|
|
2913
|
+
`);
|
|
2914
|
+
process.exit(1);
|
|
2915
|
+
}
|
|
2916
|
+
if (code === "ORG_INACTIVE") {
|
|
2917
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2918
|
+
`);
|
|
2919
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Your organization is inactive; contact support.\x1B[0m
|
|
2920
|
+
`);
|
|
2921
|
+
process.exit(1);
|
|
2922
|
+
}
|
|
2923
|
+
const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
|
|
2924
|
+
if (authRetryable && interactive) {
|
|
2925
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2926
|
+
`);
|
|
2927
|
+
process.stderr.write(
|
|
2928
|
+
`\x1B[33m[PRETENSE] Enter your Pretense dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
|
|
2929
|
+
`
|
|
2930
|
+
);
|
|
2931
|
+
let entered = null;
|
|
2932
|
+
for (; ; ) {
|
|
2933
|
+
entered = await promptDashboardApiKeyAfterFailure();
|
|
2934
|
+
if (!entered) {
|
|
2935
|
+
process.stderr.write(`\x1B[31m[PRETENSE] No key entered; exiting.\x1B[0m
|
|
2936
|
+
`);
|
|
2937
|
+
process.exit(1);
|
|
2938
|
+
}
|
|
2939
|
+
if (isValidKeyShape(entered)) break;
|
|
2940
|
+
process.stderr.write(
|
|
2941
|
+
`\x1B[31m[PRETENSE] That does not look like a Pretense dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
|
|
2942
|
+
`
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
const savedPath = installDashboardKeyForCurrentProcess(entered);
|
|
2946
|
+
process.stdout.write(`\x1B[32m[PRETENSE] Saved API key to ${savedPath}\x1B[0m
|
|
2947
|
+
`);
|
|
2948
|
+
clearValidationCache();
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
|
|
2952
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2953
|
+
`);
|
|
2954
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' to fix.\x1B[0m
|
|
2955
|
+
`);
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2959
|
+
`);
|
|
2960
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' or check connectivity.\x1B[0m
|
|
2961
|
+
`);
|
|
2962
|
+
process.exit(1);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
attachDashboardLogoutWatcher(!pretenseDashboardKeyFromEnvAtLaunch);
|
|
2966
|
+
const bannerTier = tierFromDashboardPlan(
|
|
2967
|
+
getCachedValidation()?.plan,
|
|
2968
|
+
detectTier()
|
|
2969
|
+
);
|
|
2970
|
+
let port;
|
|
2971
|
+
try {
|
|
2972
|
+
port = await findAvailablePort(requestedPort, 10);
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${err.message}\x1B[0m
|
|
2975
|
+
`);
|
|
2976
|
+
process.exit(1);
|
|
2977
|
+
}
|
|
2978
|
+
if (port !== requestedPort) {
|
|
2979
|
+
process.stderr.write(
|
|
2980
|
+
`\x1B[33m[PRETENSE] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
|
|
2981
|
+
`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
printStartClientInstructions(port);
|
|
2985
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
2986
|
+
const portStr = String(port);
|
|
2987
|
+
process.stdout.write(`
|
|
2988
|
+
\x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
|
|
2989
|
+
Your code hits AI naked. We fix that.
|
|
2990
|
+
|
|
2991
|
+
Proxy: http://localhost:${portStr}
|
|
2992
|
+
Health: http://localhost:${portStr}/health
|
|
2993
|
+
Stats: http://localhost:${portStr}/stats
|
|
2994
|
+
|
|
2995
|
+
Drop-in env vars:
|
|
2996
|
+
ANTHROPIC_BASE_URL=http://localhost:${portStr}
|
|
2997
|
+
OPENAI_BASE_URL=http://localhost:${portStr}/v1
|
|
2998
|
+
GEMINI_BASE_URL=http://localhost:${portStr}/v1beta
|
|
2999
|
+
OLLAMA_HOST=http://localhost:${portStr}
|
|
3000
|
+
|
|
3001
|
+
Routes intercepted:
|
|
3002
|
+
/v1/messages -> Anthropic (Claude Code, SDK)
|
|
3003
|
+
/v1/chat/completions -> OpenAI (ChatGPT, Cursor)
|
|
3004
|
+
/v1/responses -> OpenAI (Codex CLI)
|
|
3005
|
+
/v1beta/... -> Google Gemini
|
|
3006
|
+
/api/chat, /api/generate -> Ollama (local)
|
|
3007
|
+
`);
|
|
3008
|
+
setInterval(() => {
|
|
3009
|
+
if (dashboardSavedCredentialsRevoked) return;
|
|
3010
|
+
if (!getPretenseApiKey()) return;
|
|
3011
|
+
void validateKey({
|
|
3012
|
+
force: true,
|
|
3013
|
+
verbose: opts.verbose,
|
|
3014
|
+
enforceReachability: true
|
|
3015
|
+
}).catch(() => {
|
|
3016
|
+
});
|
|
3017
|
+
}, 6e4);
|
|
3018
|
+
});
|
|
3019
|
+
})();
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/token-footer.ts
|
|
3023
|
+
import chalk3 from "chalk";
|
|
3024
|
+
var PLAN_LIMITS = {
|
|
3025
|
+
free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
|
|
3026
|
+
pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
|
|
3027
|
+
enterprise: {
|
|
3028
|
+
monthlyMutations: Number.POSITIVE_INFINITY,
|
|
3029
|
+
monthlyScans: Number.POSITIVE_INFINITY,
|
|
3030
|
+
seats: Number.POSITIVE_INFINITY,
|
|
3031
|
+
pricePerMonth: "$99/seat/month"
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
function getPlanLimits(tier) {
|
|
3035
|
+
return PLAN_LIMITS[tier];
|
|
3036
|
+
}
|
|
3037
|
+
function fmt(n) {
|
|
3038
|
+
if (!Number.isFinite(n)) return "unlimited";
|
|
3039
|
+
return n.toLocaleString("en-US");
|
|
3040
|
+
}
|
|
3041
|
+
function printTokenFooter(filesScanned, mutationsMade) {
|
|
3042
|
+
const tier = detectTier();
|
|
3043
|
+
const limits = getPlanLimits(tier);
|
|
3044
|
+
const usage = loadUsage();
|
|
3045
|
+
if (tier === "enterprise") {
|
|
3046
|
+
process.stderr.write(
|
|
3047
|
+
chalk3.gray(`
|
|
3048
|
+
${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
|
|
3049
|
+
|
|
3050
|
+
`)
|
|
3051
|
+
);
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
const used = usage.mutations;
|
|
3055
|
+
const cap = limits.monthlyMutations;
|
|
3056
|
+
const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
|
|
3057
|
+
const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
|
|
3058
|
+
process.stderr.write(
|
|
3059
|
+
`
|
|
3060
|
+
${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
|
|
3061
|
+
`
|
|
3062
|
+
);
|
|
3063
|
+
if (tier === "free") {
|
|
3064
|
+
if (pct >= 80) {
|
|
3065
|
+
process.stderr.write(
|
|
3066
|
+
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretense upgrade")}
|
|
3067
|
+
|
|
3068
|
+
`)
|
|
3069
|
+
);
|
|
3070
|
+
} else {
|
|
3071
|
+
process.stderr.write(
|
|
3072
|
+
chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
|
|
3073
|
+
|
|
3074
|
+
`)
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
} else {
|
|
3078
|
+
process.stderr.write("\n");
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// src/banner.ts
|
|
3083
|
+
import chalk4 from "chalk";
|
|
3084
|
+
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
|
|
3085
|
+
\u2588\u2588 \u2588\u2588
|
|
3086
|
+
\u2588\u2588 \u2588\u2588 Stop your code from
|
|
3087
|
+
\u2588\u2588 \u2588\u2588 leaking to any LLM.
|
|
3088
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
3089
|
+
\u2588\u2588 pretense.ai
|
|
3090
|
+
\u2588\u2588
|
|
3091
|
+
\u2588\u2588
|
|
3092
|
+
\u2588\u2588
|
|
3093
|
+
\u2588\u2588`;
|
|
3094
|
+
var ICE_BLUE = "#7DD3FC";
|
|
3095
|
+
function shouldPrint() {
|
|
3096
|
+
if (process.env.PRETENSE_NO_BANNER === "1") return false;
|
|
3097
|
+
if (!process.stdout.isTTY) return false;
|
|
3098
|
+
if (process.env.CI) return false;
|
|
3099
|
+
return true;
|
|
3100
|
+
}
|
|
3101
|
+
function tint(line) {
|
|
3102
|
+
try {
|
|
3103
|
+
return chalk4.hex(ICE_BLUE)(line);
|
|
3104
|
+
} catch {
|
|
3105
|
+
return line;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
function printBanner() {
|
|
3109
|
+
if (!shouldPrint()) return;
|
|
3110
|
+
const lines = BANNER_RAW.split("\n");
|
|
3111
|
+
for (const line of lines) {
|
|
3112
|
+
process.stderr.write(tint(line) + "\n");
|
|
3113
|
+
}
|
|
3114
|
+
process.stderr.write("\n");
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// src/commands/status.ts
|
|
3118
|
+
import chalk5 from "chalk";
|
|
3119
|
+
var PLAN_LABEL = {
|
|
3120
|
+
free: "Free",
|
|
3121
|
+
pro: "Pro",
|
|
3122
|
+
enterprise: "Enterprise"
|
|
3123
|
+
};
|
|
3124
|
+
function fmt2(n) {
|
|
3125
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
3126
|
+
return n.toLocaleString("en-US");
|
|
3127
|
+
}
|
|
3128
|
+
async function runStatus() {
|
|
3129
|
+
let remote = await fetchUsageSummary();
|
|
3130
|
+
if (!remote) {
|
|
3131
|
+
const v = await validateKey({ force: true });
|
|
3132
|
+
if (v?.valid) {
|
|
3133
|
+
remote = usageSummaryFromValidate(v);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (remote) {
|
|
3137
|
+
const plan = remote.plan;
|
|
3138
|
+
const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3139
|
+
const lines2 = [];
|
|
3140
|
+
lines2.push("");
|
|
3141
|
+
lines2.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
|
|
3142
|
+
lines2.push("");
|
|
3143
|
+
lines2.push(` Plan: ${chalk5.bold(plan)}`);
|
|
3144
|
+
lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
|
|
3145
|
+
lines2.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
|
|
3146
|
+
lines2.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
|
|
3147
|
+
lines2.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
|
|
3148
|
+
lines2.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
|
|
3149
|
+
lines2.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
3150
|
+
if (remote.keyExpiresAt) {
|
|
3151
|
+
lines2.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
|
|
3152
|
+
}
|
|
3153
|
+
lines2.push("");
|
|
3154
|
+
if (plan === "FREE") {
|
|
3155
|
+
lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3156
|
+
lines2.push("");
|
|
3157
|
+
}
|
|
3158
|
+
process.stdout.write(lines2.join("\n") + "\n");
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
const tier = detectTier();
|
|
3162
|
+
const planLimits = getPlanLimits(tier);
|
|
3163
|
+
const tierLimits = getTierLimits(tier);
|
|
3164
|
+
const usage = loadUsage();
|
|
3165
|
+
const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3166
|
+
const lines = [];
|
|
3167
|
+
lines.push("");
|
|
3168
|
+
lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
|
|
3169
|
+
lines.push("");
|
|
3170
|
+
lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
|
|
3171
|
+
lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
|
|
3172
|
+
lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
|
|
3173
|
+
lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
|
|
3174
|
+
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
3175
|
+
lines.push("");
|
|
3176
|
+
if (tier === "free") {
|
|
3177
|
+
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3178
|
+
lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
|
|
3179
|
+
lines.push("");
|
|
3180
|
+
}
|
|
3181
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/commands/upgrade.ts
|
|
3185
|
+
import chalk6 from "chalk";
|
|
3186
|
+
var PRICING_URL = "https://pretense.ai/pricing";
|
|
3187
|
+
function runUpgrade() {
|
|
3188
|
+
const tier = detectTier();
|
|
3189
|
+
const lines = [];
|
|
3190
|
+
lines.push("");
|
|
3191
|
+
lines.push(chalk6.cyan.bold(" Pretense Plans"));
|
|
3192
|
+
lines.push("");
|
|
3193
|
+
lines.push(chalk6.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
3194
|
+
lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
|
|
3195
|
+
lines.push(chalk6.gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
3196
|
+
lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
|
|
3197
|
+
lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
|
|
3198
|
+
lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
|
|
3199
|
+
lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
|
|
3200
|
+
lines.push(" \u2502 Audit log \u2502 30 days \u2502 90 days \u2502 unlimited \u2502");
|
|
3201
|
+
lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
|
|
3202
|
+
lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
|
|
3203
|
+
lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
|
|
3204
|
+
lines.push(chalk6.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
3205
|
+
lines.push("");
|
|
3206
|
+
lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
|
|
3207
|
+
lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
|
|
3208
|
+
lines.push("");
|
|
3209
|
+
lines.push(chalk6.gray(" After purchase, set:"));
|
|
3210
|
+
lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
|
3211
|
+
lines.push("");
|
|
3212
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// src/commands/credits.ts
|
|
3216
|
+
import chalk7 from "chalk";
|
|
3217
|
+
var PLAN_LABEL2 = {
|
|
3218
|
+
free: "Free",
|
|
3219
|
+
pro: "Pro",
|
|
3220
|
+
enterprise: "Enterprise"
|
|
3221
|
+
};
|
|
3222
|
+
function fmt3(n) {
|
|
3223
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
3224
|
+
return n.toLocaleString("en-US");
|
|
3225
|
+
}
|
|
3226
|
+
function progressBar(used, cap, width = 24) {
|
|
3227
|
+
if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
|
|
3228
|
+
const pct = Math.min(1, used / cap);
|
|
3229
|
+
const filled = Math.round(pct * width);
|
|
3230
|
+
const bar = "=".repeat(filled) + "-".repeat(width - filled);
|
|
3231
|
+
const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
|
|
3232
|
+
return "[" + colored + "]";
|
|
3233
|
+
}
|
|
3234
|
+
async function runCredits() {
|
|
3235
|
+
let remote = await fetchUsageSummary();
|
|
3236
|
+
if (!remote) {
|
|
3237
|
+
const v = await validateKey({ force: true });
|
|
3238
|
+
if (v?.valid) {
|
|
3239
|
+
remote = usageSummaryFromValidate(v);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
if (remote) {
|
|
3243
|
+
const used2 = remote.mutationsUsed;
|
|
3244
|
+
const cap2 = remote.monthlyMutationLimit;
|
|
3245
|
+
const remaining2 = remote.mutationsRemaining;
|
|
3246
|
+
const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
|
|
3247
|
+
const lines2 = [];
|
|
3248
|
+
lines2.push("");
|
|
3249
|
+
lines2.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
|
|
3250
|
+
lines2.push("");
|
|
3251
|
+
lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
|
|
3252
|
+
lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
3253
|
+
lines2.push("");
|
|
3254
|
+
lines2.push(` Mutations: ${progressBar(used2, cap2)} ${fmt3(used2)} / ${fmt3(cap2)}`);
|
|
3255
|
+
lines2.push(` Used: ${pct2}%`);
|
|
3256
|
+
lines2.push(` Remaining: ${fmt3(remaining2)}`);
|
|
3257
|
+
lines2.push("");
|
|
3258
|
+
if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
|
|
3259
|
+
lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3260
|
+
lines2.push("");
|
|
3261
|
+
} else if (remote.plan === "FREE") {
|
|
3262
|
+
lines2.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
3263
|
+
lines2.push("");
|
|
3264
|
+
}
|
|
3265
|
+
process.stdout.write(lines2.join("\n") + "\n");
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const tier = detectTier();
|
|
3269
|
+
const limits = getPlanLimits(tier);
|
|
3270
|
+
const usage = loadUsage();
|
|
3271
|
+
const used = usage.mutations;
|
|
3272
|
+
const cap = limits.monthlyMutations;
|
|
3273
|
+
const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
|
|
3274
|
+
const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
|
|
3275
|
+
const lines = [];
|
|
3276
|
+
lines.push("");
|
|
3277
|
+
lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
|
|
3278
|
+
lines.push("");
|
|
3279
|
+
lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
|
|
3280
|
+
lines.push(` Month: ${usage.month}`);
|
|
3281
|
+
lines.push("");
|
|
3282
|
+
lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
3283
|
+
lines.push(` Used: ${pct}%`);
|
|
3284
|
+
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
3285
|
+
lines.push("");
|
|
3286
|
+
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
3287
|
+
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3288
|
+
lines.push("");
|
|
3289
|
+
} else if (tier === "free") {
|
|
3290
|
+
lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
3291
|
+
lines.push("");
|
|
3292
|
+
}
|
|
3293
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// src/commands/logout.ts
|
|
3297
|
+
import readline2 from "readline/promises";
|
|
3298
|
+
import chalk8 from "chalk";
|
|
3299
|
+
async function runLogout(opts = {}) {
|
|
3300
|
+
if (!loadApiKey()) {
|
|
3301
|
+
console.log(chalk8.gray("No saved Pretense dashboard API key in ~/.pretense/config.json."));
|
|
3302
|
+
console.log(chalk8.gray("If you use PRETENSE_API_KEY in your shell, run unset PRETENSE_API_KEY."));
|
|
3303
|
+
return 0;
|
|
3304
|
+
}
|
|
3305
|
+
if (!opts.assumeYes) {
|
|
3306
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3307
|
+
console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
|
|
3308
|
+
console.error(chalk8.gray("Run `pretense logout --yes` to skip the prompt."));
|
|
3309
|
+
return 1;
|
|
3310
|
+
}
|
|
3311
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
3312
|
+
try {
|
|
3313
|
+
const raw = await rl.question(
|
|
3314
|
+
chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
|
|
3315
|
+
);
|
|
3316
|
+
const answer = raw.trim().toLowerCase();
|
|
3317
|
+
if (answer !== "y" && answer !== "yes") {
|
|
3318
|
+
console.log(chalk8.yellow("Logout cancelled."));
|
|
3319
|
+
return 0;
|
|
3320
|
+
}
|
|
3321
|
+
} finally {
|
|
3322
|
+
rl.close();
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
const hadEnvDashboardKey = !!process.env["PRETENSE_API_KEY"]?.trim();
|
|
3326
|
+
const { removed, path } = removeSavedDashboardCredentials();
|
|
3327
|
+
clearValidationCache();
|
|
3328
|
+
delete process.env["PRETENSE_API_KEY"];
|
|
3329
|
+
if (removed) {
|
|
3330
|
+
console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
|
|
3331
|
+
console.log(chalk8.gray("Other terminals: run unset PRETENSE_API_KEY if that variable is still exported (e.g. in ~/.zshrc)."));
|
|
3332
|
+
if (hadEnvDashboardKey) {
|
|
3333
|
+
console.log(
|
|
3334
|
+
chalk8.yellow(
|
|
3335
|
+
"This shell had PRETENSE_API_KEY set; it was cleared for this process only. Restart any running pretense proxy (Ctrl+C, then pretense start)."
|
|
3336
|
+
)
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return 0;
|
|
1387
3341
|
}
|
|
1388
3342
|
|
|
1389
3343
|
// src/index.ts
|
|
1390
|
-
|
|
3344
|
+
{
|
|
3345
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
3346
|
+
if (Number.isNaN(major) || major < 18) {
|
|
3347
|
+
process.stderr.write(
|
|
3348
|
+
`pretense requires Node.js 18 or newer (you have v${process.versions.node}).
|
|
3349
|
+
`
|
|
3350
|
+
);
|
|
3351
|
+
process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
|
|
3352
|
+
`);
|
|
3353
|
+
process.exit(1);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
var VERSION = getCliSemver();
|
|
3357
|
+
var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
|
|
3358
|
+
var PROGRESS_INTERVAL_MS = 500;
|
|
3359
|
+
var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
|
|
3360
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".rs"]);
|
|
3361
|
+
var SUPPORTED_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3362
|
+
".json",
|
|
3363
|
+
".yaml",
|
|
3364
|
+
".yml",
|
|
3365
|
+
".toml",
|
|
3366
|
+
".env",
|
|
3367
|
+
".cfg",
|
|
3368
|
+
".ini",
|
|
3369
|
+
".tf",
|
|
3370
|
+
".tfstate",
|
|
3371
|
+
".tfvars",
|
|
3372
|
+
".hcl",
|
|
3373
|
+
".properties",
|
|
3374
|
+
".conf",
|
|
3375
|
+
".xml",
|
|
3376
|
+
".plist",
|
|
3377
|
+
".pem",
|
|
3378
|
+
".key",
|
|
3379
|
+
".crt",
|
|
3380
|
+
".pub",
|
|
3381
|
+
".pfx",
|
|
3382
|
+
".p12",
|
|
3383
|
+
".htpasswd"
|
|
3384
|
+
]);
|
|
3385
|
+
var SUPPORTED_FILENAMES = /* @__PURE__ */ new Set([
|
|
3386
|
+
".env",
|
|
3387
|
+
".env.local",
|
|
3388
|
+
".env.production",
|
|
3389
|
+
".env.development",
|
|
3390
|
+
".env.staging",
|
|
3391
|
+
".env.test",
|
|
3392
|
+
".npmrc",
|
|
3393
|
+
".netrc",
|
|
3394
|
+
".pypirc",
|
|
3395
|
+
"Dockerfile",
|
|
3396
|
+
"docker-compose.yml",
|
|
3397
|
+
"docker-compose.yaml"
|
|
3398
|
+
]);
|
|
3399
|
+
function isScannableFile(filePath) {
|
|
3400
|
+
const ext = extname(filePath).toLowerCase();
|
|
3401
|
+
return SUPPORTED_EXTENSIONS.has(ext) || SUPPORTED_CONFIG_EXTENSIONS.has(ext) || SUPPORTED_FILENAMES.has(basename(filePath));
|
|
3402
|
+
}
|
|
3403
|
+
function isSourceCodeFile(filePath) {
|
|
3404
|
+
return SUPPORTED_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
3405
|
+
}
|
|
1391
3406
|
function langFromExt(filePath) {
|
|
1392
3407
|
const ext = extname(filePath).toLowerCase();
|
|
1393
3408
|
const map = {
|
|
@@ -1399,26 +3414,98 @@ function langFromExt(filePath) {
|
|
|
1399
3414
|
".cjs": "javascript",
|
|
1400
3415
|
".py": "python",
|
|
1401
3416
|
".go": "go",
|
|
1402
|
-
".java": "java"
|
|
3417
|
+
".java": "java",
|
|
3418
|
+
".cs": "csharp",
|
|
3419
|
+
".rb": "ruby",
|
|
3420
|
+
".rs": "rust"
|
|
1403
3421
|
};
|
|
1404
3422
|
return map[ext] ?? "typescript";
|
|
1405
3423
|
}
|
|
3424
|
+
function validatePort(value) {
|
|
3425
|
+
const port = parseInt(value, 10);
|
|
3426
|
+
if (isNaN(port) || port.toString() !== value.trim()) return null;
|
|
3427
|
+
if (port < 1 || port > 65535) return null;
|
|
3428
|
+
return port;
|
|
3429
|
+
}
|
|
3430
|
+
function isBinaryFile(filePath) {
|
|
3431
|
+
let fd;
|
|
3432
|
+
try {
|
|
3433
|
+
fd = openSync(filePath, "r");
|
|
3434
|
+
const buf = Buffer.alloc(8192);
|
|
3435
|
+
const bytesRead = readSync2(fd, buf, 0, 8192, 0);
|
|
3436
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
3437
|
+
if (buf[i] === 0) return true;
|
|
3438
|
+
}
|
|
3439
|
+
return false;
|
|
3440
|
+
} catch {
|
|
3441
|
+
return false;
|
|
3442
|
+
} finally {
|
|
3443
|
+
if (fd !== void 0) closeSync(fd);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
async function isBinaryFileAsync(filePath) {
|
|
3447
|
+
let handle;
|
|
3448
|
+
try {
|
|
3449
|
+
handle = await fsOpen(filePath, "r");
|
|
3450
|
+
const buf = Buffer.alloc(8192);
|
|
3451
|
+
const { bytesRead } = await handle.read(buf, 0, 8192, 0);
|
|
3452
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
3453
|
+
if (buf[i] === 0) return true;
|
|
3454
|
+
}
|
|
3455
|
+
return false;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return false;
|
|
3458
|
+
} finally {
|
|
3459
|
+
if (handle) await handle.close().catch(() => {
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
function validateLanguage(lang) {
|
|
3464
|
+
const normalized = lang.toLowerCase();
|
|
3465
|
+
const aliases = {
|
|
3466
|
+
ts: "typescript",
|
|
3467
|
+
tsx: "typescript",
|
|
3468
|
+
js: "javascript",
|
|
3469
|
+
jsx: "javascript",
|
|
3470
|
+
py: "python",
|
|
3471
|
+
cs: "csharp",
|
|
3472
|
+
rb: "ruby",
|
|
3473
|
+
rs: "rust"
|
|
3474
|
+
};
|
|
3475
|
+
const resolved = aliases[normalized] ?? normalized;
|
|
3476
|
+
return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
|
|
3477
|
+
}
|
|
1406
3478
|
function collectFiles(target) {
|
|
1407
3479
|
const abs = resolve2(target);
|
|
1408
|
-
if (!
|
|
3480
|
+
if (!existsSync7(abs)) {
|
|
1409
3481
|
return [];
|
|
1410
3482
|
}
|
|
1411
3483
|
const stat = statSync(abs);
|
|
1412
3484
|
if (stat.isFile()) return [abs];
|
|
1413
3485
|
const files = [];
|
|
1414
|
-
const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
|
|
1415
3486
|
function walk(dir) {
|
|
1416
|
-
|
|
1417
|
-
|
|
3487
|
+
let entries;
|
|
3488
|
+
try {
|
|
3489
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3490
|
+
} catch {
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
3494
|
+
"node_modules",
|
|
3495
|
+
".git",
|
|
3496
|
+
"dist",
|
|
3497
|
+
"build",
|
|
3498
|
+
".pretense",
|
|
3499
|
+
".next",
|
|
3500
|
+
"__pycache__",
|
|
3501
|
+
".Trash"
|
|
3502
|
+
]);
|
|
3503
|
+
for (const entry of entries) {
|
|
3504
|
+
const fullPath = join5(dir, entry.name);
|
|
1418
3505
|
if (entry.isDirectory()) {
|
|
1419
|
-
if (
|
|
3506
|
+
if (skipDirs.has(entry.name)) continue;
|
|
1420
3507
|
walk(fullPath);
|
|
1421
|
-
} else if (entry.isFile() &&
|
|
3508
|
+
} else if (entry.isFile() && isScannableFile(entry.name)) {
|
|
1422
3509
|
files.push(fullPath);
|
|
1423
3510
|
}
|
|
1424
3511
|
}
|
|
@@ -1427,10 +3514,12 @@ function collectFiles(target) {
|
|
|
1427
3514
|
return files;
|
|
1428
3515
|
}
|
|
1429
3516
|
var program = new Command();
|
|
1430
|
-
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION)
|
|
3517
|
+
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
|
|
3518
|
+
printBanner();
|
|
3519
|
+
});
|
|
1431
3520
|
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
1432
3521
|
const dir = resolve2(opts.dir);
|
|
1433
|
-
console.log(
|
|
3522
|
+
console.log(chalk9.cyan("Initializing Pretense in"), chalk9.bold(dir));
|
|
1434
3523
|
const configDir = initConfig(dir);
|
|
1435
3524
|
const files = collectFiles(dir);
|
|
1436
3525
|
const langCounts = {};
|
|
@@ -1438,133 +3527,323 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
1438
3527
|
const lang = langFromExt(f);
|
|
1439
3528
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
1440
3529
|
}
|
|
1441
|
-
console.log(
|
|
1442
|
-
console.log(
|
|
3530
|
+
console.log(chalk9.green("\n .pretense/ created at"), chalk9.bold(configDir));
|
|
3531
|
+
console.log(chalk9.gray(`
|
|
1443
3532
|
Scanned ${files.length} source files:`));
|
|
1444
3533
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
1445
|
-
console.log(
|
|
3534
|
+
console.log(chalk9.gray(` ${lang}: ${count} files`));
|
|
1446
3535
|
}
|
|
1447
|
-
console.log(
|
|
3536
|
+
console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretense start"), chalk9.cyan("to launch the proxy"));
|
|
1448
3537
|
console.log();
|
|
1449
3538
|
});
|
|
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) => {
|
|
3539
|
+
program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
|
|
1451
3540
|
const config = loadConfig();
|
|
1452
|
-
const port =
|
|
3541
|
+
const port = validatePort(opts.port);
|
|
3542
|
+
if (port === null) {
|
|
3543
|
+
console.error(chalk9.red("Error: Invalid port number:"), chalk9.bold(opts.port));
|
|
3544
|
+
console.error(chalk9.gray("Port must be an integer between 1 and 65535."));
|
|
3545
|
+
process.exit(1);
|
|
3546
|
+
}
|
|
3547
|
+
await ensureDashboardKeyForStart();
|
|
3548
|
+
logDashboardCredentialHint();
|
|
1453
3549
|
startProxy({ config, port, verbose: opts.verbose });
|
|
1454
3550
|
});
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
3551
|
+
async function scanOneFile(file, opts) {
|
|
3552
|
+
let size = 0;
|
|
3553
|
+
try {
|
|
3554
|
+
const st = await fsStat(file);
|
|
3555
|
+
size = st.size;
|
|
3556
|
+
if (st.size > opts.maxFileSize) {
|
|
3557
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "too-large", sizeBytes: size };
|
|
3558
|
+
}
|
|
3559
|
+
} catch {
|
|
3560
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error" };
|
|
1460
3561
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
3562
|
+
if (!isSourceCodeFile(file)) {
|
|
3563
|
+
try {
|
|
3564
|
+
if (await isBinaryFileAsync(file)) {
|
|
3565
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "binary", sizeBytes: size };
|
|
3566
|
+
}
|
|
3567
|
+
} catch {
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
let code;
|
|
3571
|
+
try {
|
|
3572
|
+
code = await readFile(file, "utf-8");
|
|
3573
|
+
} catch {
|
|
3574
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error", sizeBytes: size };
|
|
3575
|
+
}
|
|
3576
|
+
const lang = langFromExt(file);
|
|
3577
|
+
let identifiers = 0;
|
|
3578
|
+
if (!opts.secretsOnly && isSourceCodeFile(file)) {
|
|
3579
|
+
try {
|
|
1469
3580
|
const scanResult = scan(code, lang);
|
|
1470
3581
|
identifiers = scanResult.tokens.length;
|
|
1471
|
-
|
|
3582
|
+
} catch {
|
|
1472
3583
|
}
|
|
3584
|
+
}
|
|
3585
|
+
let blocked = [];
|
|
3586
|
+
try {
|
|
1473
3587
|
const secretResult = scan2(code);
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
3588
|
+
blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
|
|
3589
|
+
} catch {
|
|
3590
|
+
}
|
|
3591
|
+
return {
|
|
3592
|
+
file,
|
|
3593
|
+
identifiers,
|
|
3594
|
+
secrets: blocked.length,
|
|
3595
|
+
secretTypes: blocked.map((m) => m.type),
|
|
3596
|
+
sizeBytes: size
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
async function scanFilesParallel(files, opts) {
|
|
3600
|
+
const results = [];
|
|
3601
|
+
let cursor = 0;
|
|
3602
|
+
let done = 0;
|
|
3603
|
+
let lastPrint = 0;
|
|
3604
|
+
let interrupted = false;
|
|
3605
|
+
const stopHandler = () => {
|
|
3606
|
+
interrupted = true;
|
|
3607
|
+
};
|
|
3608
|
+
process.on("SIGINT", stopHandler);
|
|
3609
|
+
process.on("SIGTERM", stopHandler);
|
|
3610
|
+
const isTTY = process.stderr.isTTY === true;
|
|
3611
|
+
const printProgress = (force = false) => {
|
|
3612
|
+
if (opts.quiet) return;
|
|
3613
|
+
const now = Date.now();
|
|
3614
|
+
if (!force && now - lastPrint < PROGRESS_INTERVAL_MS) return;
|
|
3615
|
+
lastPrint = now;
|
|
3616
|
+
const found = results.reduce((acc, r) => acc + r.secrets, 0);
|
|
3617
|
+
const msg = `Scanning ${done}/${files.length} files (${found} secrets so far)...`;
|
|
3618
|
+
if (isTTY) {
|
|
3619
|
+
process.stderr.write(`\r\x1B[K${msg}`);
|
|
3620
|
+
} else {
|
|
3621
|
+
process.stderr.write(`${msg}
|
|
3622
|
+
`);
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
async function worker() {
|
|
3626
|
+
while (!interrupted) {
|
|
3627
|
+
const i = cursor++;
|
|
3628
|
+
if (i >= files.length) return;
|
|
3629
|
+
const file = files[i];
|
|
3630
|
+
const row = await scanOneFile(file, { secretsOnly: opts.secretsOnly, maxFileSize: opts.maxFileSize });
|
|
3631
|
+
if (row) results.push(row);
|
|
3632
|
+
done++;
|
|
3633
|
+
printProgress();
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
printProgress(true);
|
|
3637
|
+
const workers = Array.from({ length: Math.min(opts.concurrency, files.length || 1) }, () => worker());
|
|
3638
|
+
await Promise.all(workers);
|
|
3639
|
+
process.removeListener("SIGINT", stopHandler);
|
|
3640
|
+
process.removeListener("SIGTERM", stopHandler);
|
|
3641
|
+
if (!opts.quiet && isTTY) {
|
|
3642
|
+
process.stderr.write(`\r\x1B[K`);
|
|
3643
|
+
}
|
|
3644
|
+
return { results, interrupted };
|
|
3645
|
+
}
|
|
3646
|
+
function parseSizeOption(value) {
|
|
3647
|
+
const trimmed = value.trim().toLowerCase();
|
|
3648
|
+
const m = /^(\d+(?:\.\d+)?)\s*(b|k|kb|m|mb|g|gb)?$/.exec(trimmed);
|
|
3649
|
+
if (!m) return null;
|
|
3650
|
+
const n = parseFloat(m[1]);
|
|
3651
|
+
if (!isFinite(n) || n <= 0) return null;
|
|
3652
|
+
const unit = m[2] ?? "b";
|
|
3653
|
+
const mult = unit.startsWith("g") ? 1024 ** 3 : unit.startsWith("m") ? 1024 ** 2 : unit.startsWith("k") ? 1024 : 1;
|
|
3654
|
+
return Math.floor(n * mult);
|
|
3655
|
+
}
|
|
3656
|
+
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).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
|
|
3657
|
+
const absTarget = resolve2(target);
|
|
3658
|
+
if (!existsSync7(absTarget)) {
|
|
3659
|
+
console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
|
|
3660
|
+
process.exit(1);
|
|
3661
|
+
}
|
|
3662
|
+
const maxFileSize = parseSizeOption(opts.maxFileSize);
|
|
3663
|
+
if (maxFileSize === null) {
|
|
3664
|
+
console.error(chalk9.red("Error: Invalid --max-file-size value:"), chalk9.bold(opts.maxFileSize));
|
|
3665
|
+
console.error(chalk9.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
|
|
3666
|
+
process.exit(1);
|
|
3667
|
+
}
|
|
3668
|
+
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
|
|
3669
|
+
if (!opts.quiet && !opts.json) {
|
|
3670
|
+
process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
|
|
3671
|
+
`));
|
|
3672
|
+
}
|
|
3673
|
+
const files = collectFiles(target);
|
|
3674
|
+
if (files.length === 0) {
|
|
3675
|
+
console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
|
|
3676
|
+
console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
3677
|
+
process.exit(1);
|
|
3678
|
+
}
|
|
3679
|
+
if (!opts.quiet && !opts.json) {
|
|
3680
|
+
process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
|
|
3681
|
+
`));
|
|
1482
3682
|
}
|
|
3683
|
+
const startedAt = Date.now();
|
|
3684
|
+
const { results: allResults, interrupted } = await scanFilesParallel(files, {
|
|
3685
|
+
secretsOnly: opts.secretsOnly,
|
|
3686
|
+
maxFileSize,
|
|
3687
|
+
quiet: opts.quiet || opts.json,
|
|
3688
|
+
concurrency
|
|
3689
|
+
});
|
|
3690
|
+
const elapsedMs = Date.now() - startedAt;
|
|
3691
|
+
const totalIdentifiers = allResults.reduce((acc, r) => acc + r.identifiers, 0);
|
|
3692
|
+
const totalSecrets = allResults.reduce((acc, r) => acc + r.secrets, 0);
|
|
3693
|
+
const skippedLarge = allResults.filter((r) => r.skipped === "too-large").length;
|
|
3694
|
+
const skippedBinary = allResults.filter((r) => r.skipped === "binary").length;
|
|
1483
3695
|
if (opts.json) {
|
|
1484
|
-
console.log(JSON.stringify({
|
|
3696
|
+
console.log(JSON.stringify({
|
|
3697
|
+
files: allResults,
|
|
3698
|
+
totalIdentifiers,
|
|
3699
|
+
totalSecrets,
|
|
3700
|
+
skippedLarge,
|
|
3701
|
+
skippedBinary,
|
|
3702
|
+
elapsedMs,
|
|
3703
|
+
interrupted
|
|
3704
|
+
}, null, 2));
|
|
1485
3705
|
} else {
|
|
1486
|
-
console.log(
|
|
3706
|
+
console.log(chalk9.cyan("\nPretense Scan Results\n"));
|
|
1487
3707
|
for (const r of allResults) {
|
|
1488
|
-
|
|
3708
|
+
if (r.skipped) {
|
|
3709
|
+
if (r.skipped === "too-large") {
|
|
3710
|
+
const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
|
|
3711
|
+
console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
|
|
3712
|
+
}
|
|
3713
|
+
continue;
|
|
3714
|
+
}
|
|
3715
|
+
const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
3716
|
+
const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
|
|
3717
|
+
if (!showRow) continue;
|
|
1489
3718
|
console.log(
|
|
1490
|
-
|
|
3719
|
+
chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
1491
3720
|
);
|
|
1492
3721
|
if (r.secrets > 0) {
|
|
1493
3722
|
for (const t of r.secretTypes) {
|
|
1494
|
-
console.log(
|
|
3723
|
+
console.log(chalk9.red(` ! ${t}`));
|
|
1495
3724
|
}
|
|
1496
3725
|
}
|
|
1497
3726
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
3727
|
+
const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
|
|
3728
|
+
console.log(
|
|
3729
|
+
chalk9.gray(`
|
|
3730
|
+
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk9.red(`${totalSecrets} secrets found`) : chalk9.green("0 secrets")) + skipNote
|
|
3731
|
+
);
|
|
3732
|
+
if (interrupted) {
|
|
3733
|
+
console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
|
|
3734
|
+
}
|
|
1500
3735
|
console.log();
|
|
1501
3736
|
}
|
|
3737
|
+
if (!opts.json) {
|
|
3738
|
+
printTokenFooter(allResults.length, totalIdentifiers);
|
|
3739
|
+
}
|
|
3740
|
+
if (interrupted) {
|
|
3741
|
+
process.exit(130);
|
|
3742
|
+
}
|
|
1502
3743
|
if (totalSecrets > 0) {
|
|
1503
3744
|
process.exit(2);
|
|
1504
3745
|
}
|
|
1505
3746
|
});
|
|
1506
|
-
program.command("mutate <file>").description("Mutate
|
|
3747
|
+
program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
|
|
1507
3748
|
const absPath = resolve2(file);
|
|
1508
|
-
if (!
|
|
1509
|
-
console.error(
|
|
3749
|
+
if (!existsSync7(absPath)) {
|
|
3750
|
+
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
1510
3751
|
process.exit(1);
|
|
1511
3752
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
3753
|
+
if (opts.language) {
|
|
3754
|
+
const validLang = validateLanguage(opts.language);
|
|
3755
|
+
if (!validLang) {
|
|
3756
|
+
console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
|
|
3757
|
+
console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
|
|
3758
|
+
process.exit(1);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
|
|
3762
|
+
console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
|
|
3763
|
+
console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
3764
|
+
process.exit(1);
|
|
3765
|
+
}
|
|
3766
|
+
const code = readFileSync7(absPath, "utf-8");
|
|
3767
|
+
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
3768
|
+
const secretScan = scan2(code);
|
|
3769
|
+
const effectiveSeed = opts.seed === "pretense" ? buildSaltedSeed(opts.seed) : opts.seed;
|
|
3770
|
+
const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
|
|
3771
|
+
const secretsRedacted = secretsMap.size;
|
|
3772
|
+
const result = mutate(redacted, lang, opts.seed);
|
|
1515
3773
|
if (opts.save) {
|
|
1516
3774
|
const configDir = getConfigDir();
|
|
1517
|
-
const store = new MutationStore(
|
|
3775
|
+
const store = new MutationStore(join5(configDir, "mutation-map.json"));
|
|
1518
3776
|
store.load();
|
|
1519
3777
|
store.save({
|
|
1520
3778
|
id: absPath,
|
|
1521
3779
|
originalHash: absPath,
|
|
1522
3780
|
mapEntries: MutationStore.fromMap(result.map),
|
|
3781
|
+
secretsMapEntries: [...secretsMap.entries()],
|
|
1523
3782
|
timestamp: Date.now(),
|
|
1524
3783
|
language: lang
|
|
1525
3784
|
});
|
|
1526
3785
|
store.persist();
|
|
1527
|
-
process.stderr.write(
|
|
3786
|
+
process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
1528
3787
|
`));
|
|
1529
3788
|
}
|
|
1530
3789
|
if (opts.json) {
|
|
1531
3790
|
console.log(JSON.stringify({
|
|
1532
3791
|
mutatedCode: result.mutatedCode,
|
|
1533
3792
|
map: Object.fromEntries(result.map),
|
|
1534
|
-
stats: result.stats
|
|
3793
|
+
stats: result.stats,
|
|
3794
|
+
secretsRedacted,
|
|
3795
|
+
secretsMap: Object.fromEntries(secretsMap)
|
|
1535
3796
|
}, null, 2));
|
|
1536
3797
|
} else {
|
|
1537
3798
|
process.stdout.write(result.mutatedCode);
|
|
1538
|
-
|
|
3799
|
+
if (secretsRedacted > 0) {
|
|
3800
|
+
process.stderr.write(
|
|
3801
|
+
chalk9.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before identifier mutation ---
|
|
3802
|
+
`)
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
process.stderr.write(chalk9.gray(`
|
|
1539
3806
|
--- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
|
|
1540
3807
|
`));
|
|
3808
|
+
printTokenFooter(1, result.stats.tokensMutated);
|
|
1541
3809
|
}
|
|
1542
3810
|
});
|
|
1543
3811
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
1544
3812
|
const absPath = resolve2(file);
|
|
1545
|
-
if (!
|
|
1546
|
-
console.error(
|
|
3813
|
+
if (!existsSync7(absPath)) {
|
|
3814
|
+
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
1547
3815
|
process.exit(1);
|
|
1548
3816
|
}
|
|
1549
|
-
const mutatedCode =
|
|
1550
|
-
const mapPath = opts.map ? resolve2(opts.map) :
|
|
1551
|
-
if (!
|
|
1552
|
-
console.error(
|
|
1553
|
-
console.error(
|
|
3817
|
+
const mutatedCode = readFileSync7(absPath, "utf-8");
|
|
3818
|
+
const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
|
|
3819
|
+
if (!existsSync7(mapPath)) {
|
|
3820
|
+
console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
|
|
3821
|
+
console.error(chalk9.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
|
|
3822
|
+
process.exit(1);
|
|
3823
|
+
}
|
|
3824
|
+
let store;
|
|
3825
|
+
try {
|
|
3826
|
+
store = new MutationStore(mapPath);
|
|
3827
|
+
store.load();
|
|
3828
|
+
} catch {
|
|
3829
|
+
console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
|
|
3830
|
+
console.error(chalk9.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
|
|
1554
3831
|
process.exit(1);
|
|
1555
3832
|
}
|
|
1556
|
-
const store = new MutationStore(mapPath);
|
|
1557
|
-
store.load();
|
|
1558
3833
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
1559
3834
|
if (!entry) {
|
|
1560
|
-
console.error(
|
|
3835
|
+
console.error(chalk9.red("Error: No mutation map entries found."));
|
|
3836
|
+
console.error(chalk9.gray("Run 'pretense mutate --save <file>' first to generate a map."));
|
|
1561
3837
|
process.exit(1);
|
|
1562
3838
|
}
|
|
1563
3839
|
const map = MutationStore.toMap(entry);
|
|
1564
|
-
const
|
|
3840
|
+
const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
|
|
3841
|
+
const restored = reverse(mutatedCode, map, secretsMap);
|
|
1565
3842
|
process.stdout.write(restored);
|
|
1566
|
-
|
|
1567
|
-
|
|
3843
|
+
const secretsCount = secretsMap?.size ?? 0;
|
|
3844
|
+
const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
|
|
3845
|
+
process.stderr.write(chalk9.gray(`
|
|
3846
|
+
--- Reversed ${map.size} mutations${secretsSuffix} ---
|
|
1568
3847
|
`));
|
|
1569
3848
|
});
|
|
1570
3849
|
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) => {
|
|
@@ -1577,5 +3856,39 @@ program.command("audit").description("View the audit log").option("--json", "Out
|
|
|
1577
3856
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
1578
3857
|
console.log(`@pretense/cli v${VERSION}`);
|
|
1579
3858
|
});
|
|
3859
|
+
program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
|
|
3860
|
+
await runStatus();
|
|
3861
|
+
});
|
|
3862
|
+
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
|
|
3863
|
+
await runStatus();
|
|
3864
|
+
});
|
|
3865
|
+
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(async () => {
|
|
3866
|
+
await runCredits();
|
|
3867
|
+
});
|
|
3868
|
+
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
3869
|
+
runUpgrade();
|
|
3870
|
+
});
|
|
3871
|
+
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
|
|
3872
|
+
await runCredits();
|
|
3873
|
+
});
|
|
3874
|
+
program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
|
|
3875
|
+
const code = runLogin({ key: opts.key });
|
|
3876
|
+
if (code !== 0) process.exit(code);
|
|
3877
|
+
});
|
|
3878
|
+
program.command("logout").description("Remove the saved Pretense dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
|
|
3879
|
+
const code = await runLogout({ assumeYes: opts.yes === true });
|
|
3880
|
+
if (code !== 0) process.exit(code);
|
|
3881
|
+
});
|
|
3882
|
+
function handleFatalError(err) {
|
|
3883
|
+
if (err instanceof Error && err.message) {
|
|
3884
|
+
console.error(chalk9.red("Error:"), err.message);
|
|
3885
|
+
} else {
|
|
3886
|
+
console.error(chalk9.red("An unexpected error occurred."));
|
|
3887
|
+
}
|
|
3888
|
+
process.exit(1);
|
|
3889
|
+
}
|
|
3890
|
+
process.on("uncaughtException", handleFatalError);
|
|
3891
|
+
process.on("unhandledRejection", handleFatalError);
|
|
3892
|
+
program.showSuggestionAfterError(true).showHelpAfterError("(use --help for available commands)");
|
|
1580
3893
|
program.parse();
|
|
1581
3894
|
//# sourceMappingURL=index.js.map
|