@pretense/cli 0.3.1 → 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 +1314 -235
- package/dist/index.js.map +1 -1
- package/package.json +22 -7
package/dist/index.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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";
|
|
7
15
|
import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
|
|
8
16
|
import { resolve as resolve2, extname, join as join5, basename } from "path";
|
|
9
17
|
import os from "os";
|
|
@@ -449,6 +457,15 @@ function scanTypeScript(code) {
|
|
|
449
457
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
450
458
|
// Multi-line comment
|
|
451
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
|
+
},
|
|
452
469
|
// Import source (the module path string)
|
|
453
470
|
{ re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
454
471
|
// Class name
|
|
@@ -456,11 +473,18 @@ function scanTypeScript(code) {
|
|
|
456
473
|
// Function/method name (function keyword)
|
|
457
474
|
{ re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
458
475
|
// Arrow function assigned to const/let/var
|
|
459
|
-
{ 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 },
|
|
460
477
|
// Method: identifier followed by (
|
|
461
478
|
{ re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
462
|
-
//
|
|
463
|
-
{ 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 },
|
|
464
488
|
// Template literals (preserve backtick wrapper)
|
|
465
489
|
{ re: /`[^`]*`/g, type: "String" /* String */ },
|
|
466
490
|
// Double-quoted strings
|
|
@@ -478,9 +502,12 @@ function scanTypeScript(code) {
|
|
|
478
502
|
if (!captureValue || captureValue.trim() === "") continue;
|
|
479
503
|
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
|
|
480
504
|
if (TS_JS_KEYWORDS.has(captureValue)) continue;
|
|
481
|
-
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;
|
|
482
506
|
if (captureValue.length <= 1) continue;
|
|
483
507
|
}
|
|
508
|
+
if (type === "Property" /* Property */ && captureValue === "meta" && code.slice(Math.max(0, captureStart - 7), captureStart) === "import.") {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
484
511
|
tokens.push({
|
|
485
512
|
type,
|
|
486
513
|
value: captureValue,
|
|
@@ -490,7 +517,18 @@ function scanTypeScript(code) {
|
|
|
490
517
|
});
|
|
491
518
|
}
|
|
492
519
|
}
|
|
493
|
-
|
|
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));
|
|
494
532
|
}
|
|
495
533
|
function scanPython(code) {
|
|
496
534
|
const tokens = [];
|
|
@@ -508,6 +546,11 @@ function scanPython(code) {
|
|
|
508
546
|
{ re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
509
547
|
// Function/method def
|
|
510
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 },
|
|
511
554
|
// Variable assignment (simple name = ...)
|
|
512
555
|
{ re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
513
556
|
// Double-quoted strings
|
|
@@ -523,7 +566,7 @@ function scanPython(code) {
|
|
|
523
566
|
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
524
567
|
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
525
568
|
if (!captureValue || captureValue.trim() === "") continue;
|
|
526
|
-
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 */) {
|
|
527
570
|
if (PYTHON_KEYWORDS.has(captureValue)) continue;
|
|
528
571
|
if (captureValue.length <= 1) continue;
|
|
529
572
|
if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
|
|
@@ -537,7 +580,16 @@ function scanPython(code) {
|
|
|
537
580
|
});
|
|
538
581
|
}
|
|
539
582
|
}
|
|
540
|
-
|
|
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));
|
|
541
593
|
}
|
|
542
594
|
function scanGo(code) {
|
|
543
595
|
const tokens = [];
|
|
@@ -850,7 +902,7 @@ function buildSaltedSeed(baseSeed) {
|
|
|
850
902
|
return `${baseSeed}:${getMutationSalt()}`;
|
|
851
903
|
}
|
|
852
904
|
|
|
853
|
-
// src/
|
|
905
|
+
// src/deterministic-id.ts
|
|
854
906
|
function hash32(str) {
|
|
855
907
|
let h = 5381;
|
|
856
908
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -868,11 +920,13 @@ function toAlphaNum(n, len) {
|
|
|
868
920
|
}
|
|
869
921
|
return result;
|
|
870
922
|
}
|
|
871
|
-
function
|
|
923
|
+
function deterministicSyntheticId(original, seed, prefix, len = 4) {
|
|
872
924
|
const combined = `${seed}:${prefix}:${original}`;
|
|
873
925
|
const h = hash32(combined);
|
|
874
926
|
return prefix + toAlphaNum(h, len);
|
|
875
927
|
}
|
|
928
|
+
|
|
929
|
+
// src/mutator.ts
|
|
876
930
|
function prefixForType(type) {
|
|
877
931
|
switch (type) {
|
|
878
932
|
case "Class" /* Class */:
|
|
@@ -884,6 +938,8 @@ function prefixForType(type) {
|
|
|
884
938
|
return "_v";
|
|
885
939
|
case "Property" /* Property */:
|
|
886
940
|
return "_prop";
|
|
941
|
+
case "HttpHeaderString" /* HttpHeaderString */:
|
|
942
|
+
return "_hk";
|
|
887
943
|
default:
|
|
888
944
|
return "_tok";
|
|
889
945
|
}
|
|
@@ -910,11 +966,24 @@ function mutate(code, language, seed = "pretense") {
|
|
|
910
966
|
if (token.type === "String" /* String */) {
|
|
911
967
|
continue;
|
|
912
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
|
+
}
|
|
913
982
|
if (token.type === "Import" /* Import */) {
|
|
914
983
|
continue;
|
|
915
984
|
}
|
|
916
985
|
const prefix = prefixForType(token.type);
|
|
917
|
-
const synthetic =
|
|
986
|
+
const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
|
|
918
987
|
map.set(token.value, synthetic);
|
|
919
988
|
}
|
|
920
989
|
const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
@@ -942,22 +1011,31 @@ function mutate(code, language, seed = "pretense") {
|
|
|
942
1011
|
}
|
|
943
1012
|
|
|
944
1013
|
// src/reverser.ts
|
|
945
|
-
function reverse(mutatedCode, map) {
|
|
946
|
-
if (!mutatedCode
|
|
947
|
-
const reverseMap = /* @__PURE__ */ new Map();
|
|
948
|
-
for (const [original, synthetic] of map.entries()) {
|
|
949
|
-
if (synthetic === "" || !synthetic) continue;
|
|
950
|
-
reverseMap.set(synthetic, original);
|
|
951
|
-
}
|
|
952
|
-
if (reverseMap.size === 0) return mutatedCode;
|
|
953
|
-
const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
1014
|
+
function reverse(mutatedCode, map, secretsMap) {
|
|
1015
|
+
if (!mutatedCode) return mutatedCode;
|
|
954
1016
|
let result = mutatedCode;
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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);
|
|
961
1039
|
}
|
|
962
1040
|
}
|
|
963
1041
|
return result;
|
|
@@ -966,16 +1044,40 @@ function reverse(mutatedCode, map) {
|
|
|
966
1044
|
// src/secrets.ts
|
|
967
1045
|
var SECRET_PATTERNS = [
|
|
968
1046
|
{ name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
|
|
969
|
-
|
|
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 },
|
|
970
1049
|
{ name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
971
|
-
|
|
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 },
|
|
972
1065
|
{ name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
|
|
973
1066
|
{ name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
|
|
974
1067
|
{ name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
|
|
975
|
-
{ name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
976
|
-
|
|
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 },
|
|
977
1071
|
{ name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
|
|
978
|
-
{
|
|
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
|
+
},
|
|
979
1081
|
{ name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
|
|
980
1082
|
{ name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
|
|
981
1083
|
{ name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
|
|
@@ -1009,7 +1111,28 @@ var SECRET_PATTERNS = [
|
|
|
1009
1111
|
{ name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
|
|
1010
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 },
|
|
1011
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 },
|
|
1012
|
-
|
|
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
|
+
}
|
|
1013
1136
|
];
|
|
1014
1137
|
var PII_PATTERNS = [
|
|
1015
1138
|
{ name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
@@ -1076,7 +1199,17 @@ function scan2(text, actionOverrides) {
|
|
|
1076
1199
|
def.pattern.lastIndex = 0;
|
|
1077
1200
|
let m;
|
|
1078
1201
|
while ((m = def.pattern.exec(text)) !== null) {
|
|
1079
|
-
|
|
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
|
+
}
|
|
1080
1213
|
if (def.validate && !def.validate(value)) continue;
|
|
1081
1214
|
const action = actionOverrides?.[def.name] ?? def.defaultAction;
|
|
1082
1215
|
matchCounter++;
|
|
@@ -1086,8 +1219,8 @@ function scan2(text, actionOverrides) {
|
|
|
1086
1219
|
type: def.name,
|
|
1087
1220
|
value,
|
|
1088
1221
|
masked: maskValue(value, def.name),
|
|
1089
|
-
start:
|
|
1090
|
-
end
|
|
1222
|
+
start: start2,
|
|
1223
|
+
end,
|
|
1091
1224
|
severity: def.severity,
|
|
1092
1225
|
action
|
|
1093
1226
|
});
|
|
@@ -1151,14 +1284,26 @@ function scan2(text, actionOverrides) {
|
|
|
1151
1284
|
};
|
|
1152
1285
|
}
|
|
1153
1286
|
function applyRedactions(text, matches) {
|
|
1154
|
-
|
|
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
|
+
}
|
|
1155
1301
|
let result = text;
|
|
1156
1302
|
for (const match of sorted) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1303
|
+
const tag = placeholders.get(match.id);
|
|
1304
|
+
result = result.slice(0, match.start) + tag + result.slice(match.end);
|
|
1160
1305
|
}
|
|
1161
|
-
return result;
|
|
1306
|
+
return { redactedCode: result, secretsMap };
|
|
1162
1307
|
}
|
|
1163
1308
|
|
|
1164
1309
|
// src/store.ts
|
|
@@ -1405,6 +1550,11 @@ function detectTier() {
|
|
|
1405
1550
|
if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
|
|
1406
1551
|
return "free";
|
|
1407
1552
|
}
|
|
1553
|
+
function tierFromDashboardPlan(plan, fallback) {
|
|
1554
|
+
if (plan === "ENTERPRISE") return "enterprise";
|
|
1555
|
+
if (plan === "PRO") return "pro";
|
|
1556
|
+
return fallback;
|
|
1557
|
+
}
|
|
1408
1558
|
function getTierLimits(tier) {
|
|
1409
1559
|
switch (tier) {
|
|
1410
1560
|
case "enterprise":
|
|
@@ -1512,6 +1662,7 @@ import { Hono } from "hono";
|
|
|
1512
1662
|
import { serve } from "@hono/node-server";
|
|
1513
1663
|
import { nanoid } from "nanoid";
|
|
1514
1664
|
import { createServer } from "net";
|
|
1665
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
1515
1666
|
|
|
1516
1667
|
// src/auth.ts
|
|
1517
1668
|
import { createHash } from "crypto";
|
|
@@ -1575,7 +1726,657 @@ function sessionCount() {
|
|
|
1575
1726
|
return sessions.size;
|
|
1576
1727
|
}
|
|
1577
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
|
+
|
|
1578
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
|
+
}
|
|
1579
2380
|
function anthropicUpstream() {
|
|
1580
2381
|
return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
|
|
1581
2382
|
}
|
|
@@ -1606,6 +2407,12 @@ function detectProvider(path) {
|
|
|
1606
2407
|
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
|
|
1607
2408
|
return "anthropic";
|
|
1608
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
|
+
}
|
|
1609
2416
|
function extractText(body) {
|
|
1610
2417
|
const parts = [];
|
|
1611
2418
|
if (typeof body["system"] === "string") parts.push(body["system"]);
|
|
@@ -1688,7 +2495,7 @@ function createProxy(opts = {}) {
|
|
|
1688
2495
|
"/",
|
|
1689
2496
|
(c) => c.json({
|
|
1690
2497
|
name: "pretense",
|
|
1691
|
-
version:
|
|
2498
|
+
version: getCliSemver(),
|
|
1692
2499
|
status: "running",
|
|
1693
2500
|
routes: {
|
|
1694
2501
|
"GET /": "This welcome page",
|
|
@@ -1705,7 +2512,7 @@ function createProxy(opts = {}) {
|
|
|
1705
2512
|
"/health",
|
|
1706
2513
|
(c) => c.json({
|
|
1707
2514
|
status: "ok",
|
|
1708
|
-
version:
|
|
2515
|
+
version: getCliSemver(),
|
|
1709
2516
|
uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
|
|
1710
2517
|
stats: {
|
|
1711
2518
|
requestsProcessed: stats.requestsProcessed,
|
|
@@ -1735,6 +2542,7 @@ function createProxy(opts = {}) {
|
|
|
1735
2542
|
});
|
|
1736
2543
|
delete headers2["host"];
|
|
1737
2544
|
headers2["host"] = new URL(upstream2).host;
|
|
2545
|
+
stripPretenseOnlyForwardingHeaders(headers2);
|
|
1738
2546
|
const resp = await fetch(url, { method: c.req.method, headers: headers2 });
|
|
1739
2547
|
return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
|
|
1740
2548
|
}
|
|
@@ -1751,9 +2559,69 @@ function createProxy(opts = {}) {
|
|
|
1751
2559
|
const requestId = nanoid(12);
|
|
1752
2560
|
const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1753
2561
|
const provider = detectProvider(c.req.path);
|
|
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
|
+
}
|
|
1754
2619
|
const sessHash = c.get("sessionHash");
|
|
1755
2620
|
const session = getSession(sessHash);
|
|
1756
|
-
const tier =
|
|
2621
|
+
const tier = tierFromDashboardPlan(
|
|
2622
|
+
getCachedValidation()?.plan,
|
|
2623
|
+
detectTier()
|
|
2624
|
+
);
|
|
1757
2625
|
const { monthlyMutations } = getTierLimits(tier);
|
|
1758
2626
|
const usage = loadUsage();
|
|
1759
2627
|
if (tier === "free" && usage.mutations >= monthlyMutations) {
|
|
@@ -1767,6 +2635,30 @@ function createProxy(opts = {}) {
|
|
|
1767
2635
|
429
|
|
1768
2636
|
);
|
|
1769
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
|
+
}
|
|
1770
2662
|
const fullText = extractText(body);
|
|
1771
2663
|
const secretScan = scan2(fullText);
|
|
1772
2664
|
const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
|
|
@@ -1790,7 +2682,17 @@ function createProxy(opts = {}) {
|
|
|
1790
2682
|
let tokensMutated = 0;
|
|
1791
2683
|
const mutatedBody = applyToBody(processedBody, (text) => {
|
|
1792
2684
|
const blocks = extractCodeBlocks(text);
|
|
1793
|
-
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
|
+
}
|
|
1794
2696
|
const mutatedBlocks = [];
|
|
1795
2697
|
for (const block of blocks) {
|
|
1796
2698
|
const result = mutate(block.code, block.language);
|
|
@@ -1806,6 +2708,20 @@ function createProxy(opts = {}) {
|
|
|
1806
2708
|
});
|
|
1807
2709
|
usage.mutations += tokensMutated;
|
|
1808
2710
|
saveUsage(usage);
|
|
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
|
+
}
|
|
1809
2725
|
const warningThreshold = Math.floor(monthlyMutations * 0.8);
|
|
1810
2726
|
if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
|
|
1811
2727
|
printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
|
|
@@ -1839,6 +2755,7 @@ function createProxy(opts = {}) {
|
|
|
1839
2755
|
delete headers["content-length"];
|
|
1840
2756
|
headers["host"] = new URL(upstream).host;
|
|
1841
2757
|
headers["x-pretense-request-id"] = requestId;
|
|
2758
|
+
stripPretenseOnlyForwardingHeaders(headers);
|
|
1842
2759
|
const forwardBody = JSON.stringify(mutatedBody);
|
|
1843
2760
|
let upstreamResp;
|
|
1844
2761
|
try {
|
|
@@ -1953,8 +2870,103 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
1953
2870
|
function startProxy(opts = {}) {
|
|
1954
2871
|
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
1955
2872
|
const app = createProxy(opts);
|
|
1956
|
-
const tier = detectTier();
|
|
1957
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
|
|
2911
|
+
`);
|
|
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
|
+
);
|
|
1958
2970
|
let port;
|
|
1959
2971
|
try {
|
|
1960
2972
|
port = await findAvailablePort(requestedPort, 10);
|
|
@@ -1969,10 +2981,11 @@ function startProxy(opts = {}) {
|
|
|
1969
2981
|
`
|
|
1970
2982
|
);
|
|
1971
2983
|
}
|
|
2984
|
+
printStartClientInstructions(port);
|
|
1972
2985
|
serve({ fetch: app.fetch, port }, () => {
|
|
1973
2986
|
const portStr = String(port);
|
|
1974
2987
|
process.stdout.write(`
|
|
1975
|
-
\x1B[36mPretense
|
|
2988
|
+
\x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
|
|
1976
2989
|
Your code hits AI naked. We fix that.
|
|
1977
2990
|
|
|
1978
2991
|
Proxy: http://localhost:${portStr}
|
|
@@ -1992,12 +3005,22 @@ function startProxy(opts = {}) {
|
|
|
1992
3005
|
/v1beta/... -> Google Gemini
|
|
1993
3006
|
/api/chat, /api/generate -> Ollama (local)
|
|
1994
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);
|
|
1995
3018
|
});
|
|
1996
3019
|
})();
|
|
1997
3020
|
}
|
|
1998
3021
|
|
|
1999
3022
|
// src/token-footer.ts
|
|
2000
|
-
import
|
|
3023
|
+
import chalk3 from "chalk";
|
|
2001
3024
|
var PLAN_LIMITS = {
|
|
2002
3025
|
free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
|
|
2003
3026
|
pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
|
|
@@ -2021,7 +3044,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
2021
3044
|
const usage = loadUsage();
|
|
2022
3045
|
if (tier === "enterprise") {
|
|
2023
3046
|
process.stderr.write(
|
|
2024
|
-
|
|
3047
|
+
chalk3.gray(`
|
|
2025
3048
|
${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
|
|
2026
3049
|
|
|
2027
3050
|
`)
|
|
@@ -2031,22 +3054,22 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
2031
3054
|
const used = usage.mutations;
|
|
2032
3055
|
const cap = limits.monthlyMutations;
|
|
2033
3056
|
const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
|
|
2034
|
-
const usageStr = pct >= 80 ?
|
|
3057
|
+
const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
|
|
2035
3058
|
process.stderr.write(
|
|
2036
3059
|
`
|
|
2037
|
-
${
|
|
3060
|
+
${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
|
|
2038
3061
|
`
|
|
2039
3062
|
);
|
|
2040
3063
|
if (tier === "free") {
|
|
2041
3064
|
if (pct >= 80) {
|
|
2042
3065
|
process.stderr.write(
|
|
2043
|
-
|
|
3066
|
+
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretense upgrade")}
|
|
2044
3067
|
|
|
2045
3068
|
`)
|
|
2046
3069
|
);
|
|
2047
3070
|
} else {
|
|
2048
3071
|
process.stderr.write(
|
|
2049
|
-
|
|
3072
|
+
chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
|
|
2050
3073
|
|
|
2051
3074
|
`)
|
|
2052
3075
|
);
|
|
@@ -2057,7 +3080,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
2057
3080
|
}
|
|
2058
3081
|
|
|
2059
3082
|
// src/banner.ts
|
|
2060
|
-
import
|
|
3083
|
+
import chalk4 from "chalk";
|
|
2061
3084
|
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
|
|
2062
3085
|
\u2588\u2588 \u2588\u2588
|
|
2063
3086
|
\u2588\u2588 \u2588\u2588 Stop your code from
|
|
@@ -2077,7 +3100,7 @@ function shouldPrint() {
|
|
|
2077
3100
|
}
|
|
2078
3101
|
function tint(line) {
|
|
2079
3102
|
try {
|
|
2080
|
-
return
|
|
3103
|
+
return chalk4.hex(ICE_BLUE)(line);
|
|
2081
3104
|
} catch {
|
|
2082
3105
|
return line;
|
|
2083
3106
|
}
|
|
@@ -2092,53 +3115,85 @@ function printBanner() {
|
|
|
2092
3115
|
}
|
|
2093
3116
|
|
|
2094
3117
|
// src/commands/status.ts
|
|
2095
|
-
import
|
|
3118
|
+
import chalk5 from "chalk";
|
|
2096
3119
|
var PLAN_LABEL = {
|
|
2097
3120
|
free: "Free",
|
|
2098
3121
|
pro: "Pro",
|
|
2099
3122
|
enterprise: "Enterprise"
|
|
2100
3123
|
};
|
|
2101
3124
|
function fmt2(n) {
|
|
2102
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3125
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
2103
3126
|
return n.toLocaleString("en-US");
|
|
2104
3127
|
}
|
|
2105
|
-
function runStatus() {
|
|
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
|
+
}
|
|
2106
3161
|
const tier = detectTier();
|
|
2107
3162
|
const planLimits = getPlanLimits(tier);
|
|
2108
3163
|
const tierLimits = getTierLimits(tier);
|
|
2109
3164
|
const usage = loadUsage();
|
|
2110
|
-
const tierBadge = tier === "enterprise" ?
|
|
3165
|
+
const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
2111
3166
|
const lines = [];
|
|
2112
3167
|
lines.push("");
|
|
2113
|
-
lines.push(`${
|
|
3168
|
+
lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
|
|
2114
3169
|
lines.push("");
|
|
2115
|
-
lines.push(` Plan: ${
|
|
3170
|
+
lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
|
|
2116
3171
|
lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
|
|
2117
3172
|
lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
|
|
2118
3173
|
lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
|
|
2119
3174
|
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
2120
3175
|
lines.push("");
|
|
2121
3176
|
if (tier === "free") {
|
|
2122
|
-
lines.push(` ${
|
|
2123
|
-
lines.push(` ${
|
|
3177
|
+
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3178
|
+
lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
|
|
2124
3179
|
lines.push("");
|
|
2125
3180
|
}
|
|
2126
3181
|
process.stdout.write(lines.join("\n") + "\n");
|
|
2127
3182
|
}
|
|
2128
3183
|
|
|
2129
3184
|
// src/commands/upgrade.ts
|
|
2130
|
-
import
|
|
3185
|
+
import chalk6 from "chalk";
|
|
2131
3186
|
var PRICING_URL = "https://pretense.ai/pricing";
|
|
2132
3187
|
function runUpgrade() {
|
|
2133
3188
|
const tier = detectTier();
|
|
2134
3189
|
const lines = [];
|
|
2135
3190
|
lines.push("");
|
|
2136
|
-
lines.push(
|
|
3191
|
+
lines.push(chalk6.cyan.bold(" Pretense Plans"));
|
|
2137
3192
|
lines.push("");
|
|
2138
|
-
lines.push(
|
|
2139
|
-
lines.push(
|
|
2140
|
-
lines.push(
|
|
2141
|
-
lines.push(` \u2502 Price/seat \u2502 ${
|
|
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`);
|
|
2142
3197
|
lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
|
|
2143
3198
|
lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
|
|
2144
3199
|
lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
|
|
@@ -2146,37 +3201,70 @@ function runUpgrade() {
|
|
|
2146
3201
|
lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2147
3202
|
lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2148
3203
|
lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
|
|
2149
|
-
lines.push(
|
|
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"));
|
|
2150
3205
|
lines.push("");
|
|
2151
|
-
lines.push(` ${
|
|
2152
|
-
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)}`);
|
|
2153
3208
|
lines.push("");
|
|
2154
|
-
lines.push(
|
|
2155
|
-
lines.push(
|
|
3209
|
+
lines.push(chalk6.gray(" After purchase, set:"));
|
|
3210
|
+
lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
|
2156
3211
|
lines.push("");
|
|
2157
3212
|
process.stdout.write(lines.join("\n") + "\n");
|
|
2158
3213
|
}
|
|
2159
3214
|
|
|
2160
3215
|
// src/commands/credits.ts
|
|
2161
|
-
import
|
|
3216
|
+
import chalk7 from "chalk";
|
|
2162
3217
|
var PLAN_LABEL2 = {
|
|
2163
3218
|
free: "Free",
|
|
2164
3219
|
pro: "Pro",
|
|
2165
3220
|
enterprise: "Enterprise"
|
|
2166
3221
|
};
|
|
2167
3222
|
function fmt3(n) {
|
|
2168
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3223
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
2169
3224
|
return n.toLocaleString("en-US");
|
|
2170
3225
|
}
|
|
2171
3226
|
function progressBar(used, cap, width = 24) {
|
|
2172
|
-
if (!Number.isFinite(cap) || cap <= 0) return
|
|
3227
|
+
if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
|
|
2173
3228
|
const pct = Math.min(1, used / cap);
|
|
2174
3229
|
const filled = Math.round(pct * width);
|
|
2175
3230
|
const bar = "=".repeat(filled) + "-".repeat(width - filled);
|
|
2176
|
-
const colored = pct >= 0.8 ?
|
|
3231
|
+
const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
|
|
2177
3232
|
return "[" + colored + "]";
|
|
2178
3233
|
}
|
|
2179
|
-
function runCredits() {
|
|
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
|
+
}
|
|
2180
3268
|
const tier = detectTier();
|
|
2181
3269
|
const limits = getPlanLimits(tier);
|
|
2182
3270
|
const usage = loadUsage();
|
|
@@ -2186,9 +3274,9 @@ function runCredits() {
|
|
|
2186
3274
|
const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
|
|
2187
3275
|
const lines = [];
|
|
2188
3276
|
lines.push("");
|
|
2189
|
-
lines.push(
|
|
3277
|
+
lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
|
|
2190
3278
|
lines.push("");
|
|
2191
|
-
lines.push(` Plan: ${
|
|
3279
|
+
lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
|
|
2192
3280
|
lines.push(` Month: ${usage.month}`);
|
|
2193
3281
|
lines.push("");
|
|
2194
3282
|
lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
@@ -2196,106 +3284,59 @@ function runCredits() {
|
|
|
2196
3284
|
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
2197
3285
|
lines.push("");
|
|
2198
3286
|
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
2199
|
-
lines.push(
|
|
3287
|
+
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
2200
3288
|
lines.push("");
|
|
2201
3289
|
} else if (tier === "free") {
|
|
2202
|
-
lines.push(
|
|
3290
|
+
lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
2203
3291
|
lines.push("");
|
|
2204
3292
|
}
|
|
2205
3293
|
process.stdout.write(lines.join("\n") + "\n");
|
|
2206
3294
|
}
|
|
2207
3295
|
|
|
2208
|
-
// src/commands/
|
|
2209
|
-
import
|
|
2210
|
-
import
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
}
|
|
2224
|
-
function readStdinIfPiped() {
|
|
2225
|
-
if (process.stdin.isTTY) return "";
|
|
2226
|
-
try {
|
|
2227
|
-
const chunks = [];
|
|
2228
|
-
const buf = Buffer.alloc(4096);
|
|
2229
|
-
let bytesRead = 0;
|
|
2230
|
-
do {
|
|
2231
|
-
try {
|
|
2232
|
-
bytesRead = readSync(0, buf, 0, buf.length, null);
|
|
2233
|
-
} catch {
|
|
2234
|
-
bytesRead = 0;
|
|
2235
|
-
}
|
|
2236
|
-
if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
2237
|
-
} while (bytesRead === buf.length);
|
|
2238
|
-
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
2239
|
-
} catch {
|
|
2240
|
-
return "";
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
function maskKey(key) {
|
|
2244
|
-
const trimmed = key.trim();
|
|
2245
|
-
if (trimmed.length <= 12) return trimmed;
|
|
2246
|
-
return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
|
|
2247
|
-
}
|
|
2248
|
-
function saveApiKey(key, configDir = getUserConfigDir()) {
|
|
2249
|
-
if (!existsSync5(configDir)) {
|
|
2250
|
-
mkdirSync5(configDir, { recursive: true });
|
|
2251
|
-
}
|
|
2252
|
-
const path = join4(configDir, CONFIG_FILE2);
|
|
2253
|
-
let existing = {};
|
|
2254
|
-
if (existsSync5(path)) {
|
|
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 });
|
|
2255
3312
|
try {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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();
|
|
2259
3323
|
}
|
|
2260
3324
|
}
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
if (!candidate) {
|
|
2276
|
-
console.error(chalk6.red("Error: no API key provided."));
|
|
2277
|
-
console.error(
|
|
2278
|
-
chalk6.gray("Try: pretense login --key pk_live_xxxxx")
|
|
2279
|
-
);
|
|
2280
|
-
console.error(
|
|
2281
|
-
chalk6.gray(" or: echo $PRETENSE_API_KEY | pretense login")
|
|
2282
|
-
);
|
|
2283
|
-
return 1;
|
|
2284
|
-
}
|
|
2285
|
-
if (!isValidKeyShape(candidate)) {
|
|
2286
|
-
console.error(chalk6.red("Error: that does not look like a Pretense API key."));
|
|
2287
|
-
console.error(
|
|
2288
|
-
chalk6.gray("Keys start with pk_live_ and are at least 24 characters long.")
|
|
2289
|
-
);
|
|
2290
|
-
console.error(
|
|
2291
|
-
chalk6.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
|
|
2292
|
-
);
|
|
2293
|
-
return 1;
|
|
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
|
+
}
|
|
2294
3339
|
}
|
|
2295
|
-
const path = saveApiKey(candidate, opts.configDir);
|
|
2296
|
-
console.log(chalk6.green("Saved API key to"), chalk6.bold(path));
|
|
2297
|
-
console.log(chalk6.gray(` ${maskKey(candidate)}`));
|
|
2298
|
-
console.log(chalk6.gray(" File mode 600 \u2014 readable only by you."));
|
|
2299
3340
|
return 0;
|
|
2300
3341
|
}
|
|
2301
3342
|
|
|
@@ -2312,7 +3353,7 @@ function runLogin(opts = {}) {
|
|
|
2312
3353
|
process.exit(1);
|
|
2313
3354
|
}
|
|
2314
3355
|
}
|
|
2315
|
-
var VERSION =
|
|
3356
|
+
var VERSION = getCliSemver();
|
|
2316
3357
|
var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
|
|
2317
3358
|
var PROGRESS_INTERVAL_MS = 500;
|
|
2318
3359
|
var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
|
|
@@ -2436,17 +3477,33 @@ function validateLanguage(lang) {
|
|
|
2436
3477
|
}
|
|
2437
3478
|
function collectFiles(target) {
|
|
2438
3479
|
const abs = resolve2(target);
|
|
2439
|
-
if (!
|
|
3480
|
+
if (!existsSync7(abs)) {
|
|
2440
3481
|
return [];
|
|
2441
3482
|
}
|
|
2442
3483
|
const stat = statSync(abs);
|
|
2443
3484
|
if (stat.isFile()) return [abs];
|
|
2444
3485
|
const files = [];
|
|
2445
3486
|
function walk(dir) {
|
|
2446
|
-
|
|
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) {
|
|
2447
3504
|
const fullPath = join5(dir, entry.name);
|
|
2448
3505
|
if (entry.isDirectory()) {
|
|
2449
|
-
if (
|
|
3506
|
+
if (skipDirs.has(entry.name)) continue;
|
|
2450
3507
|
walk(fullPath);
|
|
2451
3508
|
} else if (entry.isFile() && isScannableFile(entry.name)) {
|
|
2452
3509
|
files.push(fullPath);
|
|
@@ -2462,7 +3519,7 @@ program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary
|
|
|
2462
3519
|
});
|
|
2463
3520
|
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
2464
3521
|
const dir = resolve2(opts.dir);
|
|
2465
|
-
console.log(
|
|
3522
|
+
console.log(chalk9.cyan("Initializing Pretense in"), chalk9.bold(dir));
|
|
2466
3523
|
const configDir = initConfig(dir);
|
|
2467
3524
|
const files = collectFiles(dir);
|
|
2468
3525
|
const langCounts = {};
|
|
@@ -2470,23 +3527,25 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
2470
3527
|
const lang = langFromExt(f);
|
|
2471
3528
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
2472
3529
|
}
|
|
2473
|
-
console.log(
|
|
2474
|
-
console.log(
|
|
3530
|
+
console.log(chalk9.green("\n .pretense/ created at"), chalk9.bold(configDir));
|
|
3531
|
+
console.log(chalk9.gray(`
|
|
2475
3532
|
Scanned ${files.length} source files:`));
|
|
2476
3533
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
2477
|
-
console.log(
|
|
3534
|
+
console.log(chalk9.gray(` ${lang}: ${count} files`));
|
|
2478
3535
|
}
|
|
2479
|
-
console.log(
|
|
3536
|
+
console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretense start"), chalk9.cyan("to launch the proxy"));
|
|
2480
3537
|
console.log();
|
|
2481
3538
|
});
|
|
2482
|
-
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) => {
|
|
2483
3540
|
const config = loadConfig();
|
|
2484
3541
|
const port = validatePort(opts.port);
|
|
2485
3542
|
if (port === null) {
|
|
2486
|
-
console.error(
|
|
2487
|
-
console.error(
|
|
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."));
|
|
2488
3545
|
process.exit(1);
|
|
2489
3546
|
}
|
|
3547
|
+
await ensureDashboardKeyForStart();
|
|
3548
|
+
logDashboardCredentialHint();
|
|
2490
3549
|
startProxy({ config, port, verbose: opts.verbose });
|
|
2491
3550
|
});
|
|
2492
3551
|
async function scanOneFile(file, opts) {
|
|
@@ -2596,29 +3655,29 @@ function parseSizeOption(value) {
|
|
|
2596
3655
|
}
|
|
2597
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) => {
|
|
2598
3657
|
const absTarget = resolve2(target);
|
|
2599
|
-
if (!
|
|
2600
|
-
console.error(
|
|
3658
|
+
if (!existsSync7(absTarget)) {
|
|
3659
|
+
console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
|
|
2601
3660
|
process.exit(1);
|
|
2602
3661
|
}
|
|
2603
3662
|
const maxFileSize = parseSizeOption(opts.maxFileSize);
|
|
2604
3663
|
if (maxFileSize === null) {
|
|
2605
|
-
console.error(
|
|
2606
|
-
console.error(
|
|
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"));
|
|
2607
3666
|
process.exit(1);
|
|
2608
3667
|
}
|
|
2609
3668
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
|
|
2610
3669
|
if (!opts.quiet && !opts.json) {
|
|
2611
|
-
process.stderr.write(
|
|
3670
|
+
process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
|
|
2612
3671
|
`));
|
|
2613
3672
|
}
|
|
2614
3673
|
const files = collectFiles(target);
|
|
2615
3674
|
if (files.length === 0) {
|
|
2616
|
-
console.error(
|
|
2617
|
-
console.error(
|
|
3675
|
+
console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
|
|
3676
|
+
console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
2618
3677
|
process.exit(1);
|
|
2619
3678
|
}
|
|
2620
3679
|
if (!opts.quiet && !opts.json) {
|
|
2621
|
-
process.stderr.write(
|
|
3680
|
+
process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
|
|
2622
3681
|
`));
|
|
2623
3682
|
}
|
|
2624
3683
|
const startedAt = Date.now();
|
|
@@ -2644,34 +3703,34 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
2644
3703
|
interrupted
|
|
2645
3704
|
}, null, 2));
|
|
2646
3705
|
} else {
|
|
2647
|
-
console.log(
|
|
3706
|
+
console.log(chalk9.cyan("\nPretense Scan Results\n"));
|
|
2648
3707
|
for (const r of allResults) {
|
|
2649
3708
|
if (r.skipped) {
|
|
2650
3709
|
if (r.skipped === "too-large") {
|
|
2651
3710
|
const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
|
|
2652
|
-
console.log(
|
|
3711
|
+
console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
|
|
2653
3712
|
}
|
|
2654
3713
|
continue;
|
|
2655
3714
|
}
|
|
2656
|
-
const secretBadge = r.secrets > 0 ?
|
|
3715
|
+
const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
2657
3716
|
const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
|
|
2658
3717
|
if (!showRow) continue;
|
|
2659
3718
|
console.log(
|
|
2660
|
-
|
|
3719
|
+
chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
2661
3720
|
);
|
|
2662
3721
|
if (r.secrets > 0) {
|
|
2663
3722
|
for (const t of r.secretTypes) {
|
|
2664
|
-
console.log(
|
|
3723
|
+
console.log(chalk9.red(` ! ${t}`));
|
|
2665
3724
|
}
|
|
2666
3725
|
}
|
|
2667
3726
|
}
|
|
2668
|
-
const skipNote = skippedLarge + skippedBinary > 0 ?
|
|
3727
|
+
const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
|
|
2669
3728
|
console.log(
|
|
2670
|
-
|
|
2671
|
-
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ?
|
|
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
|
|
2672
3731
|
);
|
|
2673
3732
|
if (interrupted) {
|
|
2674
|
-
console.log(
|
|
3733
|
+
console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
|
|
2675
3734
|
}
|
|
2676
3735
|
console.log();
|
|
2677
3736
|
}
|
|
@@ -2685,28 +3744,32 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
2685
3744
|
process.exit(2);
|
|
2686
3745
|
}
|
|
2687
3746
|
});
|
|
2688
|
-
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) => {
|
|
2689
3748
|
const absPath = resolve2(file);
|
|
2690
|
-
if (!
|
|
2691
|
-
console.error(
|
|
3749
|
+
if (!existsSync7(absPath)) {
|
|
3750
|
+
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
2692
3751
|
process.exit(1);
|
|
2693
3752
|
}
|
|
2694
3753
|
if (opts.language) {
|
|
2695
3754
|
const validLang = validateLanguage(opts.language);
|
|
2696
3755
|
if (!validLang) {
|
|
2697
|
-
console.error(
|
|
2698
|
-
console.error(
|
|
3756
|
+
console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
|
|
3757
|
+
console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
|
|
2699
3758
|
process.exit(1);
|
|
2700
3759
|
}
|
|
2701
3760
|
}
|
|
2702
3761
|
if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
|
|
2703
|
-
console.error(
|
|
2704
|
-
console.error(
|
|
3762
|
+
console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
|
|
3763
|
+
console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
2705
3764
|
process.exit(1);
|
|
2706
3765
|
}
|
|
2707
|
-
const code =
|
|
3766
|
+
const code = readFileSync7(absPath, "utf-8");
|
|
2708
3767
|
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
2709
|
-
const
|
|
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);
|
|
2710
3773
|
if (opts.save) {
|
|
2711
3774
|
const configDir = getConfigDir();
|
|
2712
3775
|
const store = new MutationStore(join5(configDir, "mutation-map.json"));
|
|
@@ -2715,22 +3778,31 @@ program.command("mutate <file>").description("Mutate a file and print result to
|
|
|
2715
3778
|
id: absPath,
|
|
2716
3779
|
originalHash: absPath,
|
|
2717
3780
|
mapEntries: MutationStore.fromMap(result.map),
|
|
3781
|
+
secretsMapEntries: [...secretsMap.entries()],
|
|
2718
3782
|
timestamp: Date.now(),
|
|
2719
3783
|
language: lang
|
|
2720
3784
|
});
|
|
2721
3785
|
store.persist();
|
|
2722
|
-
process.stderr.write(
|
|
3786
|
+
process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
2723
3787
|
`));
|
|
2724
3788
|
}
|
|
2725
3789
|
if (opts.json) {
|
|
2726
3790
|
console.log(JSON.stringify({
|
|
2727
3791
|
mutatedCode: result.mutatedCode,
|
|
2728
3792
|
map: Object.fromEntries(result.map),
|
|
2729
|
-
stats: result.stats
|
|
3793
|
+
stats: result.stats,
|
|
3794
|
+
secretsRedacted,
|
|
3795
|
+
secretsMap: Object.fromEntries(secretsMap)
|
|
2730
3796
|
}, null, 2));
|
|
2731
3797
|
} else {
|
|
2732
3798
|
process.stdout.write(result.mutatedCode);
|
|
2733
|
-
|
|
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(`
|
|
2734
3806
|
--- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
|
|
2735
3807
|
`));
|
|
2736
3808
|
printTokenFooter(1, result.stats.tokensMutated);
|
|
@@ -2738,15 +3810,15 @@ program.command("mutate <file>").description("Mutate a file and print result to
|
|
|
2738
3810
|
});
|
|
2739
3811
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
2740
3812
|
const absPath = resolve2(file);
|
|
2741
|
-
if (!
|
|
2742
|
-
console.error(
|
|
3813
|
+
if (!existsSync7(absPath)) {
|
|
3814
|
+
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
2743
3815
|
process.exit(1);
|
|
2744
3816
|
}
|
|
2745
|
-
const mutatedCode =
|
|
3817
|
+
const mutatedCode = readFileSync7(absPath, "utf-8");
|
|
2746
3818
|
const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
|
|
2747
|
-
if (!
|
|
2748
|
-
console.error(
|
|
2749
|
-
console.error(
|
|
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>"));
|
|
2750
3822
|
process.exit(1);
|
|
2751
3823
|
}
|
|
2752
3824
|
let store;
|
|
@@ -2754,21 +3826,24 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
|
|
|
2754
3826
|
store = new MutationStore(mapPath);
|
|
2755
3827
|
store.load();
|
|
2756
3828
|
} catch {
|
|
2757
|
-
console.error(
|
|
2758
|
-
console.error(
|
|
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."));
|
|
2759
3831
|
process.exit(1);
|
|
2760
3832
|
}
|
|
2761
3833
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
2762
3834
|
if (!entry) {
|
|
2763
|
-
console.error(
|
|
2764
|
-
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."));
|
|
2765
3837
|
process.exit(1);
|
|
2766
3838
|
}
|
|
2767
3839
|
const map = MutationStore.toMap(entry);
|
|
2768
|
-
const
|
|
3840
|
+
const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
|
|
3841
|
+
const restored = reverse(mutatedCode, map, secretsMap);
|
|
2769
3842
|
process.stdout.write(restored);
|
|
2770
|
-
|
|
2771
|
-
|
|
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} ---
|
|
2772
3847
|
`));
|
|
2773
3848
|
});
|
|
2774
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) => {
|
|
@@ -2781,30 +3856,34 @@ program.command("audit").description("View the audit log").option("--json", "Out
|
|
|
2781
3856
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
2782
3857
|
console.log(`@pretense/cli v${VERSION}`);
|
|
2783
3858
|
});
|
|
2784
|
-
program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
|
|
2785
|
-
runStatus();
|
|
3859
|
+
program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
|
|
3860
|
+
await runStatus();
|
|
2786
3861
|
});
|
|
2787
|
-
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
|
|
2788
|
-
runStatus();
|
|
3862
|
+
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
|
|
3863
|
+
await runStatus();
|
|
2789
3864
|
});
|
|
2790
|
-
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(() => {
|
|
2791
|
-
runCredits();
|
|
3865
|
+
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(async () => {
|
|
3866
|
+
await runCredits();
|
|
2792
3867
|
});
|
|
2793
3868
|
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
2794
3869
|
runUpgrade();
|
|
2795
3870
|
});
|
|
2796
|
-
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
|
|
2797
|
-
runCredits();
|
|
3871
|
+
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
|
|
3872
|
+
await runCredits();
|
|
2798
3873
|
});
|
|
2799
|
-
program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
|
|
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) => {
|
|
2800
3875
|
const code = runLogin({ key: opts.key });
|
|
2801
3876
|
if (code !== 0) process.exit(code);
|
|
2802
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
|
+
});
|
|
2803
3882
|
function handleFatalError(err) {
|
|
2804
3883
|
if (err instanceof Error && err.message) {
|
|
2805
|
-
console.error(
|
|
3884
|
+
console.error(chalk9.red("Error:"), err.message);
|
|
2806
3885
|
} else {
|
|
2807
|
-
console.error(
|
|
3886
|
+
console.error(chalk9.red("An unexpected error occurred."));
|
|
2808
3887
|
}
|
|
2809
3888
|
process.exit(1);
|
|
2810
3889
|
}
|