@pretense/cli 0.3.1 → 0.5.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 +70 -57
- package/dist/index.js +1673 -419
- package/dist/index.js.map +1 -1
- package/package.json +57 -42
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
|
+
import chalk10 from "chalk";
|
|
6
|
+
import {
|
|
7
|
+
readFileSync as readFileSync8,
|
|
8
|
+
existsSync as existsSync8,
|
|
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
|
-
import { resolve as
|
|
16
|
+
import { resolve as resolve3, extname, join as join6, basename } from "path";
|
|
9
17
|
import os from "os";
|
|
10
18
|
|
|
11
19
|
// src/types.ts
|
|
@@ -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 = [];
|
|
@@ -849,8 +901,14 @@ function getMutationSalt() {
|
|
|
849
901
|
function buildSaltedSeed(baseSeed) {
|
|
850
902
|
return `${baseSeed}:${getMutationSalt()}`;
|
|
851
903
|
}
|
|
904
|
+
function effectiveSeedForMutation(userSeed) {
|
|
905
|
+
if (userSeed === "pretense" || userSeed === "pretest") {
|
|
906
|
+
return buildSaltedSeed("pretense");
|
|
907
|
+
}
|
|
908
|
+
return userSeed;
|
|
909
|
+
}
|
|
852
910
|
|
|
853
|
-
// src/
|
|
911
|
+
// src/deterministic-id.ts
|
|
854
912
|
function hash32(str) {
|
|
855
913
|
let h = 5381;
|
|
856
914
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -868,11 +926,13 @@ function toAlphaNum(n, len) {
|
|
|
868
926
|
}
|
|
869
927
|
return result;
|
|
870
928
|
}
|
|
871
|
-
function
|
|
929
|
+
function deterministicSyntheticId(original, seed, prefix, len = 4) {
|
|
872
930
|
const combined = `${seed}:${prefix}:${original}`;
|
|
873
931
|
const h = hash32(combined);
|
|
874
932
|
return prefix + toAlphaNum(h, len);
|
|
875
933
|
}
|
|
934
|
+
|
|
935
|
+
// src/mutator.ts
|
|
876
936
|
function prefixForType(type) {
|
|
877
937
|
switch (type) {
|
|
878
938
|
case "Class" /* Class */:
|
|
@@ -884,6 +944,8 @@ function prefixForType(type) {
|
|
|
884
944
|
return "_v";
|
|
885
945
|
case "Property" /* Property */:
|
|
886
946
|
return "_prop";
|
|
947
|
+
case "HttpHeaderString" /* HttpHeaderString */:
|
|
948
|
+
return "_hk";
|
|
887
949
|
default:
|
|
888
950
|
return "_tok";
|
|
889
951
|
}
|
|
@@ -910,11 +972,24 @@ function mutate(code, language, seed = "pretense") {
|
|
|
910
972
|
if (token.type === "String" /* String */) {
|
|
911
973
|
continue;
|
|
912
974
|
}
|
|
975
|
+
if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
|
|
976
|
+
const qm = /^("|')(.+)\1$/.exec(token.value);
|
|
977
|
+
if (qm) {
|
|
978
|
+
const inner = qm[2];
|
|
979
|
+
const q = qm[1];
|
|
980
|
+
const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
|
|
981
|
+
map.set(token.value, `${q}${syntheticInner}${q}`);
|
|
982
|
+
} else {
|
|
983
|
+
const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
|
|
984
|
+
map.set(token.value, syntheticInner);
|
|
985
|
+
}
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
913
988
|
if (token.type === "Import" /* Import */) {
|
|
914
989
|
continue;
|
|
915
990
|
}
|
|
916
991
|
const prefix = prefixForType(token.type);
|
|
917
|
-
const synthetic =
|
|
992
|
+
const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
|
|
918
993
|
map.set(token.value, synthetic);
|
|
919
994
|
}
|
|
920
995
|
const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
@@ -942,22 +1017,31 @@ function mutate(code, language, seed = "pretense") {
|
|
|
942
1017
|
}
|
|
943
1018
|
|
|
944
1019
|
// 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);
|
|
1020
|
+
function reverse(mutatedCode, map, secretsMap) {
|
|
1021
|
+
if (!mutatedCode) return mutatedCode;
|
|
954
1022
|
let result = mutatedCode;
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1023
|
+
if (map.size > 0) {
|
|
1024
|
+
const reverseMap = /* @__PURE__ */ new Map();
|
|
1025
|
+
for (const [original, synthetic] of map.entries()) {
|
|
1026
|
+
if (synthetic === "" || !synthetic) continue;
|
|
1027
|
+
reverseMap.set(synthetic, original);
|
|
1028
|
+
}
|
|
1029
|
+
if (reverseMap.size > 0) {
|
|
1030
|
+
const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
1031
|
+
for (const [synthetic, original] of sortedEntries) {
|
|
1032
|
+
const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1033
|
+
if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
|
|
1034
|
+
result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
|
|
1035
|
+
} else {
|
|
1036
|
+
result = result.split(synthetic).join(original);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (secretsMap && secretsMap.size > 0) {
|
|
1042
|
+
const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
1043
|
+
for (const [placeholder, original] of sortedSecrets) {
|
|
1044
|
+
result = result.split(placeholder).join(original);
|
|
961
1045
|
}
|
|
962
1046
|
}
|
|
963
1047
|
return result;
|
|
@@ -966,16 +1050,40 @@ function reverse(mutatedCode, map) {
|
|
|
966
1050
|
// src/secrets.ts
|
|
967
1051
|
var SECRET_PATTERNS = [
|
|
968
1052
|
{ name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
|
|
969
|
-
|
|
1053
|
+
/** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with @pretense/scanner */
|
|
1054
|
+
{ name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
|
|
970
1055
|
{ name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
971
|
-
|
|
1056
|
+
/** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
|
|
1057
|
+
{ name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
|
|
1058
|
+
/**
|
|
1059
|
+
* Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
|
|
1060
|
+
* Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
|
|
1061
|
+
*/
|
|
1062
|
+
{
|
|
1063
|
+
name: "aws-secret-key-delimited",
|
|
1064
|
+
category: "secret",
|
|
1065
|
+
severity: "critical",
|
|
1066
|
+
defaultAction: "block",
|
|
1067
|
+
pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
|
|
1068
|
+
valueGroup: 1
|
|
1069
|
+
},
|
|
1070
|
+
{ 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
1071
|
{ name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
|
|
973
1072
|
{ name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
|
|
974
1073
|
{ 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
|
-
|
|
1074
|
+
{ 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 },
|
|
1075
|
+
/** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
|
|
1076
|
+
{ 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
1077
|
{ name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
|
|
978
|
-
{
|
|
1078
|
+
{
|
|
1079
|
+
name: "generic-password",
|
|
1080
|
+
category: "credential",
|
|
1081
|
+
severity: "high",
|
|
1082
|
+
defaultAction: "redact",
|
|
1083
|
+
// Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
|
|
1084
|
+
pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
|
|
1085
|
+
valueGroup: 1
|
|
1086
|
+
},
|
|
979
1087
|
{ name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
|
|
980
1088
|
{ name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
|
|
981
1089
|
{ name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
|
|
@@ -1009,7 +1117,28 @@ var SECRET_PATTERNS = [
|
|
|
1009
1117
|
{ name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
|
|
1010
1118
|
{ 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
1119
|
{ 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
|
-
|
|
1120
|
+
/** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
|
|
1121
|
+
{ name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
|
|
1122
|
+
/** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
|
|
1123
|
+
{ name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
|
|
1124
|
+
/** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
|
|
1125
|
+
{
|
|
1126
|
+
name: "env-password-assignment",
|
|
1127
|
+
category: "credential",
|
|
1128
|
+
severity: "high",
|
|
1129
|
+
defaultAction: "block",
|
|
1130
|
+
pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
|
|
1131
|
+
valueGroup: 1
|
|
1132
|
+
},
|
|
1133
|
+
/** Quoted common DB dialect names — stack hints when mutating env-style config. */
|
|
1134
|
+
{
|
|
1135
|
+
name: "quoted-db-dialect",
|
|
1136
|
+
category: "credential",
|
|
1137
|
+
severity: "medium",
|
|
1138
|
+
defaultAction: "block",
|
|
1139
|
+
pattern: /(['"])(postgres|mysql|redis)\1/gd,
|
|
1140
|
+
valueGroup: 2
|
|
1141
|
+
}
|
|
1013
1142
|
];
|
|
1014
1143
|
var PII_PATTERNS = [
|
|
1015
1144
|
{ name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
@@ -1076,7 +1205,17 @@ function scan2(text, actionOverrides) {
|
|
|
1076
1205
|
def.pattern.lastIndex = 0;
|
|
1077
1206
|
let m;
|
|
1078
1207
|
while ((m = def.pattern.exec(text)) !== null) {
|
|
1079
|
-
|
|
1208
|
+
let value = m[0];
|
|
1209
|
+
let start2 = m.index;
|
|
1210
|
+
let end = start2 + value.length;
|
|
1211
|
+
if (def.valueGroup !== void 0) {
|
|
1212
|
+
const span = m.indices?.[def.valueGroup];
|
|
1213
|
+
if (!span) continue;
|
|
1214
|
+
const [gs, ge] = span;
|
|
1215
|
+
value = text.slice(gs, ge);
|
|
1216
|
+
start2 = gs;
|
|
1217
|
+
end = ge;
|
|
1218
|
+
}
|
|
1080
1219
|
if (def.validate && !def.validate(value)) continue;
|
|
1081
1220
|
const action = actionOverrides?.[def.name] ?? def.defaultAction;
|
|
1082
1221
|
matchCounter++;
|
|
@@ -1086,8 +1225,8 @@ function scan2(text, actionOverrides) {
|
|
|
1086
1225
|
type: def.name,
|
|
1087
1226
|
value,
|
|
1088
1227
|
masked: maskValue(value, def.name),
|
|
1089
|
-
start:
|
|
1090
|
-
end
|
|
1228
|
+
start: start2,
|
|
1229
|
+
end,
|
|
1091
1230
|
severity: def.severity,
|
|
1092
1231
|
action
|
|
1093
1232
|
});
|
|
@@ -1151,14 +1290,26 @@ function scan2(text, actionOverrides) {
|
|
|
1151
1290
|
};
|
|
1152
1291
|
}
|
|
1153
1292
|
function applyRedactions(text, matches) {
|
|
1154
|
-
|
|
1293
|
+
return applyRedactionsTracked(text, matches).redactedCode;
|
|
1294
|
+
}
|
|
1295
|
+
var SECRET_LITERAL_PREFIX = "_sl";
|
|
1296
|
+
function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
|
|
1297
|
+
const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
|
|
1298
|
+
const sorted = [...actionable].sort((a, b) => b.start - a.start);
|
|
1299
|
+
const secretsMap = /* @__PURE__ */ new Map();
|
|
1300
|
+
const placeholders = /* @__PURE__ */ new Map();
|
|
1301
|
+
const forward = [...actionable].sort((a, b) => a.start - b.start);
|
|
1302
|
+
for (const match of forward) {
|
|
1303
|
+
const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
|
|
1304
|
+
placeholders.set(match.id, tag);
|
|
1305
|
+
secretsMap.set(tag, match.value);
|
|
1306
|
+
}
|
|
1155
1307
|
let result = text;
|
|
1156
1308
|
for (const match of sorted) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1309
|
+
const tag = placeholders.get(match.id);
|
|
1310
|
+
result = result.slice(0, match.start) + tag + result.slice(match.end);
|
|
1160
1311
|
}
|
|
1161
|
-
return result;
|
|
1312
|
+
return { redactedCode: result, secretsMap };
|
|
1162
1313
|
}
|
|
1163
1314
|
|
|
1164
1315
|
// src/store.ts
|
|
@@ -1372,15 +1523,6 @@ function loadUsage(dir) {
|
|
|
1372
1523
|
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
1373
1524
|
}
|
|
1374
1525
|
}
|
|
1375
|
-
function saveUsage(usage, dir) {
|
|
1376
|
-
const configDir = getConfigDir(dir);
|
|
1377
|
-
if (!existsSync3(configDir)) {
|
|
1378
|
-
mkdirSync3(configDir, { recursive: true });
|
|
1379
|
-
}
|
|
1380
|
-
const usagePath = join2(configDir, USAGE_FILE);
|
|
1381
|
-
const checksum = computeUsageChecksum(usage);
|
|
1382
|
-
writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
|
|
1383
|
-
}
|
|
1384
1526
|
var MIN_LICENSE_KEY_LENGTH = 40;
|
|
1385
1527
|
var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
|
|
1386
1528
|
function isValidLicenseKey(key, prefix) {
|
|
@@ -1405,6 +1547,11 @@ function detectTier() {
|
|
|
1405
1547
|
if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
|
|
1406
1548
|
return "free";
|
|
1407
1549
|
}
|
|
1550
|
+
function tierFromDashboardPlan(plan, fallback) {
|
|
1551
|
+
if (plan === "ENTERPRISE") return "enterprise";
|
|
1552
|
+
if (plan === "PRO") return "pro";
|
|
1553
|
+
return fallback;
|
|
1554
|
+
}
|
|
1408
1555
|
function getTierLimits(tier) {
|
|
1409
1556
|
switch (tier) {
|
|
1410
1557
|
case "enterprise":
|
|
@@ -1491,7 +1638,7 @@ function formatText(entries) {
|
|
|
1491
1638
|
if (entries.length === 0) return "No audit entries found.";
|
|
1492
1639
|
const lines = entries.map((e) => {
|
|
1493
1640
|
const date = new Date(e.timestamp).toLocaleString();
|
|
1494
|
-
return `[${date}] ${e.file} | ${e.identifiersMutated}
|
|
1641
|
+
return `[${date}] ${e.file} | ${e.identifiersMutated} protected | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
|
|
1495
1642
|
});
|
|
1496
1643
|
return lines.join("\n");
|
|
1497
1644
|
}
|
|
@@ -1512,6 +1659,7 @@ import { Hono } from "hono";
|
|
|
1512
1659
|
import { serve } from "@hono/node-server";
|
|
1513
1660
|
import { nanoid } from "nanoid";
|
|
1514
1661
|
import { createServer } from "net";
|
|
1662
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
1515
1663
|
|
|
1516
1664
|
// src/auth.ts
|
|
1517
1665
|
import { createHash } from "crypto";
|
|
@@ -1575,111 +1723,892 @@ function sessionCount() {
|
|
|
1575
1723
|
return sessions.size;
|
|
1576
1724
|
}
|
|
1577
1725
|
|
|
1578
|
-
// src/
|
|
1579
|
-
|
|
1580
|
-
|
|
1726
|
+
// src/git-context.ts
|
|
1727
|
+
import { execFile } from "child_process";
|
|
1728
|
+
import { promisify } from "util";
|
|
1729
|
+
var execFileAsync = promisify(execFile);
|
|
1730
|
+
var GIT_TIMEOUT_MS = 2e3;
|
|
1731
|
+
async function runGit(args, cwd) {
|
|
1732
|
+
try {
|
|
1733
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
1734
|
+
cwd,
|
|
1735
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1736
|
+
windowsHide: true
|
|
1737
|
+
});
|
|
1738
|
+
const trimmed = stdout.trim();
|
|
1739
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1740
|
+
} catch {
|
|
1741
|
+
return void 0;
|
|
1742
|
+
}
|
|
1581
1743
|
}
|
|
1582
|
-
function
|
|
1583
|
-
|
|
1744
|
+
function sanitizeRemote(raw) {
|
|
1745
|
+
if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
|
|
1746
|
+
return raw;
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
const url = new URL(raw);
|
|
1750
|
+
url.username = "";
|
|
1751
|
+
url.password = "";
|
|
1752
|
+
return url.toString();
|
|
1753
|
+
} catch {
|
|
1754
|
+
return raw;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function collectGitContext(cwd = process.cwd()) {
|
|
1758
|
+
const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
1759
|
+
if (insideRepo !== "true") return {};
|
|
1760
|
+
const [remote, branch, sha] = await Promise.all([
|
|
1761
|
+
runGit(["remote", "get-url", "origin"], cwd),
|
|
1762
|
+
runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
|
|
1763
|
+
runGit(["rev-parse", "HEAD"], cwd)
|
|
1764
|
+
]);
|
|
1765
|
+
const ctx = {};
|
|
1766
|
+
if (remote) ctx.git_remote = sanitizeRemote(remote);
|
|
1767
|
+
if (branch && branch !== "HEAD") ctx.git_branch = branch;
|
|
1768
|
+
if (sha) ctx.git_commit_sha = sha;
|
|
1769
|
+
return ctx;
|
|
1770
|
+
}
|
|
1771
|
+
var CACHE_TTL_MS = 3e4;
|
|
1772
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1773
|
+
async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
|
|
1774
|
+
const hit = cache.get(cwd);
|
|
1775
|
+
if (hit && hit.expiresAt > now) return hit.value;
|
|
1776
|
+
const value = await collectGitContext(cwd);
|
|
1777
|
+
cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
|
|
1778
|
+
return value;
|
|
1584
1779
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1780
|
+
|
|
1781
|
+
// src/commands/login.ts
|
|
1782
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
|
|
1783
|
+
import { join as join4 } from "path";
|
|
1784
|
+
import { homedir as homedir2 } from "os";
|
|
1785
|
+
import readline from "readline/promises";
|
|
1786
|
+
import chalk from "chalk";
|
|
1787
|
+
var CONFIG_DIR_NAME2 = ".pretense";
|
|
1788
|
+
var CONFIG_FILE2 = "config.json";
|
|
1789
|
+
function getUserDashboardConfigDir() {
|
|
1790
|
+
return join4(homedir2(), CONFIG_DIR_NAME2);
|
|
1587
1791
|
}
|
|
1588
|
-
function
|
|
1589
|
-
return
|
|
1792
|
+
function getUserDashboardConfigPath() {
|
|
1793
|
+
return join4(getUserDashboardConfigDir(), CONFIG_FILE2);
|
|
1590
1794
|
}
|
|
1591
|
-
function
|
|
1592
|
-
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1795
|
+
function isValidKeyShape(key) {
|
|
1796
|
+
const trimmed = key.trim();
|
|
1797
|
+
if (trimmed.length < 24) return false;
|
|
1798
|
+
if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
|
|
1799
|
+
return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
|
|
1800
|
+
}
|
|
1801
|
+
function readStdinIfPiped() {
|
|
1802
|
+
if (process.stdin.isTTY) return "";
|
|
1803
|
+
try {
|
|
1804
|
+
const chunks = [];
|
|
1805
|
+
const buf = Buffer.alloc(4096);
|
|
1806
|
+
let bytesRead = 0;
|
|
1807
|
+
do {
|
|
1808
|
+
try {
|
|
1809
|
+
bytesRead = readSync(0, buf, 0, buf.length, null);
|
|
1810
|
+
} catch {
|
|
1811
|
+
bytesRead = 0;
|
|
1812
|
+
}
|
|
1813
|
+
if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
1814
|
+
} while (bytesRead === buf.length);
|
|
1815
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1816
|
+
} catch {
|
|
1817
|
+
return "";
|
|
1595
1818
|
}
|
|
1596
|
-
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
|
|
1597
|
-
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
|
|
1598
|
-
return anthropicUpstream();
|
|
1599
1819
|
}
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
if (
|
|
1603
|
-
|
|
1820
|
+
function maskKey(key) {
|
|
1821
|
+
const trimmed = key.trim();
|
|
1822
|
+
if (trimmed.length <= 12) return trimmed;
|
|
1823
|
+
return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
|
|
1824
|
+
}
|
|
1825
|
+
function logDashboardCredentialHint() {
|
|
1826
|
+
const env = process.env["PRETENSE_API_KEY"]?.trim() ?? "";
|
|
1827
|
+
const envOk = env.length > 0 && isValidKeyShape(env);
|
|
1828
|
+
const abs = getUserDashboardConfigPath();
|
|
1829
|
+
const fk = loadApiKey();
|
|
1830
|
+
if (envOk) {
|
|
1831
|
+
console.error(
|
|
1832
|
+
chalk.gray(
|
|
1833
|
+
`[pretense] Dashboard key source: PRETENSE_API_KEY in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
|
|
1834
|
+
)
|
|
1835
|
+
);
|
|
1836
|
+
} else if (fk && isValidKeyShape(fk)) {
|
|
1837
|
+
console.error(chalk.gray(`[pretense] Dashboard key source: ${abs} (${maskKey(fk)})`));
|
|
1838
|
+
} else {
|
|
1839
|
+
console.error(chalk.gray("[pretense] Dashboard key source: none configured."));
|
|
1604
1840
|
}
|
|
1605
|
-
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
|
|
1606
|
-
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
|
|
1607
|
-
return "anthropic";
|
|
1608
1841
|
}
|
|
1609
|
-
function
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1842
|
+
function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
|
|
1843
|
+
if (!existsSync5(configDir)) {
|
|
1844
|
+
mkdirSync5(configDir, { recursive: true });
|
|
1845
|
+
}
|
|
1846
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1847
|
+
let existing = {};
|
|
1848
|
+
if (existsSync5(path)) {
|
|
1849
|
+
try {
|
|
1850
|
+
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1851
|
+
} catch {
|
|
1852
|
+
existing = {};
|
|
1620
1853
|
}
|
|
1621
1854
|
}
|
|
1622
|
-
|
|
1855
|
+
const next = {
|
|
1856
|
+
...existing,
|
|
1857
|
+
api_key: key.trim(),
|
|
1858
|
+
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1859
|
+
};
|
|
1860
|
+
writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1861
|
+
try {
|
|
1862
|
+
chmodSync(path, 384);
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
return path;
|
|
1623
1866
|
}
|
|
1624
|
-
function
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
let
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1867
|
+
function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
|
|
1868
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1869
|
+
if (!existsSync5(path)) return { removed: false, path };
|
|
1870
|
+
let existing = {};
|
|
1871
|
+
try {
|
|
1872
|
+
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1873
|
+
} catch {
|
|
1874
|
+
return { removed: false, path };
|
|
1875
|
+
}
|
|
1876
|
+
const ak = existing.api_key;
|
|
1877
|
+
const hadKey = typeof ak === "string" && ak.trim().length > 0;
|
|
1878
|
+
if (!hadKey) return { removed: false, path };
|
|
1879
|
+
delete existing.api_key;
|
|
1880
|
+
delete existing.saved_at;
|
|
1881
|
+
writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1882
|
+
try {
|
|
1883
|
+
chmodSync(path, 384);
|
|
1884
|
+
} catch {
|
|
1635
1885
|
}
|
|
1636
|
-
return
|
|
1886
|
+
return { removed: true, path };
|
|
1637
1887
|
}
|
|
1638
|
-
function
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1888
|
+
function loadApiKey(configDir = getUserDashboardConfigDir()) {
|
|
1889
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
1890
|
+
if (!existsSync5(path)) return null;
|
|
1891
|
+
try {
|
|
1892
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1893
|
+
const parsed = JSON.parse(raw);
|
|
1894
|
+
if (typeof parsed.api_key === "string") {
|
|
1895
|
+
const t = parsed.api_key.trim();
|
|
1896
|
+
if (t.length > 0) return t;
|
|
1897
|
+
}
|
|
1898
|
+
} catch {
|
|
1645
1899
|
}
|
|
1646
|
-
return
|
|
1900
|
+
return null;
|
|
1647
1901
|
}
|
|
1648
|
-
function
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1651
|
-
if (
|
|
1652
|
-
|
|
1653
|
-
const obj = val;
|
|
1654
|
-
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
|
|
1902
|
+
function resolveDashboardKeyCandidate() {
|
|
1903
|
+
const env = (process.env["PRETENSE_API_KEY"] ?? "").trim();
|
|
1904
|
+
if (env.length > 0) {
|
|
1905
|
+
if (!isValidKeyShape(env)) {
|
|
1906
|
+
return { key: null, fromFile: false, badEnv: true };
|
|
1655
1907
|
}
|
|
1656
|
-
return
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1908
|
+
return { key: env, fromFile: false, badEnv: false };
|
|
1909
|
+
}
|
|
1910
|
+
const fileKey = loadApiKey();
|
|
1911
|
+
if (fileKey && isValidKeyShape(fileKey)) {
|
|
1912
|
+
return { key: fileKey.trim(), fromFile: true, badEnv: false };
|
|
1913
|
+
}
|
|
1914
|
+
return { key: null, fromFile: Boolean(fileKey), badEnv: false };
|
|
1659
1915
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1666
|
-
function
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1916
|
+
function installDashboardKeyForCurrentProcess(key) {
|
|
1917
|
+
const trimmed = key.trim();
|
|
1918
|
+
const path = saveApiKey(trimmed);
|
|
1919
|
+
process.env["PRETENSE_API_KEY"] = trimmed;
|
|
1920
|
+
return path;
|
|
1921
|
+
}
|
|
1922
|
+
async function promptDashboardApiKeyAfterFailure() {
|
|
1923
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
1924
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1925
|
+
try {
|
|
1926
|
+
const line = (await rl.question(chalk.bold("Pretense dashboard API key: "))).trim();
|
|
1927
|
+
return line.length > 0 ? line : null;
|
|
1928
|
+
} finally {
|
|
1929
|
+
rl.close();
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async function ensureDashboardKeyForStart() {
|
|
1933
|
+
const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
|
|
1934
|
+
if (badEnv) {
|
|
1935
|
+
console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
|
|
1936
|
+
console.error(
|
|
1937
|
+
chalk.gray("Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretense/config.json.")
|
|
1938
|
+
);
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
if (key) {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (fromFile) {
|
|
1945
|
+
console.warn(
|
|
1946
|
+
chalk.yellow(
|
|
1947
|
+
"Ignoring invalid `api_key` in ~/.pretense/config.json (wrong shape). Enter a new key or run `pretense login --key ...`."
|
|
1948
|
+
)
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
const stdinOk = process.stdin.isTTY;
|
|
1952
|
+
const stdoutOk = process.stdout.isTTY;
|
|
1953
|
+
if (!stdinOk || !stdoutOk) {
|
|
1954
|
+
console.error(chalk.red("Error: Pretense API key required to start the proxy."));
|
|
1955
|
+
console.error(
|
|
1956
|
+
chalk.gray(
|
|
1957
|
+
"Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
1958
|
+
)
|
|
1959
|
+
);
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1963
|
+
try {
|
|
1964
|
+
console.log(chalk.cyan("No Pretense API key found in ~/.pretense/config.json or $PRETENSE_API_KEY."));
|
|
1965
|
+
const entered = (await rl.question(chalk.bold("Enter your Pretense API key: "))).trim();
|
|
1966
|
+
if (!entered) {
|
|
1967
|
+
console.error(chalk.red("No key entered; exiting."));
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
if (!isValidKeyShape(entered)) {
|
|
1971
|
+
console.error(chalk.red("That does not look like a Pretense API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
|
|
1972
|
+
console.error(chalk.gray("Get one from your Pretense dashboard, then run `pretense login --key ...`."));
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
const path = installDashboardKeyForCurrentProcess(entered);
|
|
1976
|
+
console.log(chalk.green("Saved API key to"), chalk.bold(path));
|
|
1977
|
+
console.log(chalk.gray(` ${maskKey(entered)}`));
|
|
1978
|
+
} finally {
|
|
1979
|
+
rl.close();
|
|
1980
|
+
}
|
|
1981
|
+
const persisted = resolveDashboardKeyCandidate();
|
|
1982
|
+
if (!persisted.key || persisted.badEnv) {
|
|
1983
|
+
console.error(chalk.red("Error: Pretense dashboard API key could not be confirmed after login prompt."));
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function runLogin(opts = {}) {
|
|
1988
|
+
const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
|
|
1989
|
+
if (!candidate) {
|
|
1990
|
+
console.error(chalk.red("Error: no API key provided."));
|
|
1991
|
+
console.error(
|
|
1992
|
+
chalk.gray("Try: pretense login --key pk_live_xxxxx")
|
|
1993
|
+
);
|
|
1994
|
+
console.error(
|
|
1995
|
+
chalk.gray(" or: echo $PRETENSE_API_KEY | pretense login")
|
|
1996
|
+
);
|
|
1997
|
+
return 1;
|
|
1998
|
+
}
|
|
1999
|
+
if (!isValidKeyShape(candidate)) {
|
|
2000
|
+
console.error(chalk.red("Error: that does not look like a Pretense API key."));
|
|
2001
|
+
console.error(
|
|
2002
|
+
chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
|
|
2003
|
+
);
|
|
2004
|
+
console.error(
|
|
2005
|
+
chalk.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
|
|
2006
|
+
);
|
|
2007
|
+
return 1;
|
|
2008
|
+
}
|
|
2009
|
+
const path = saveApiKey(candidate, opts.configDir);
|
|
2010
|
+
console.log(chalk.green("Saved API key to"), chalk.bold(path));
|
|
2011
|
+
console.log(chalk.gray(` ${maskKey(candidate)}`));
|
|
2012
|
+
console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
|
|
2013
|
+
return 0;
|
|
1671
2014
|
}
|
|
1672
|
-
function printUpgradeNudge(reason) {
|
|
1673
|
-
process.stdout.write(
|
|
1674
|
-
`
|
|
1675
|
-
\x1B[33m[PRETENSE] ${reason}\x1B[0m
|
|
1676
|
-
\x1B[33m[PRETENSE] Upgrade to Pro: https://pretense.ai/pricing\x1B[0m
|
|
1677
2015
|
|
|
1678
|
-
|
|
1679
|
-
|
|
2016
|
+
// src/dashboard-env.ts
|
|
2017
|
+
var DASHBOARD_API_KEY_ENVS = ["PRETENSE_API_KEY", "PRETEST_API_KEY"];
|
|
2018
|
+
var DASHBOARD_API_URL_ENVS = ["PRETENSE_API_URL", "PRETEST_API_URL"];
|
|
2019
|
+
function firstNonEmptyEnv(names) {
|
|
2020
|
+
for (const name of names) {
|
|
2021
|
+
const v = process.env[name]?.trim();
|
|
2022
|
+
if (v) return v;
|
|
2023
|
+
}
|
|
2024
|
+
return "";
|
|
1680
2025
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
2026
|
+
function clearDashboardApiKeyFromProcess() {
|
|
2027
|
+
delete process.env["PRETEST_API_KEY"];
|
|
2028
|
+
delete process.env["PRETENSE_API_KEY"];
|
|
2029
|
+
}
|
|
2030
|
+
function hasDashboardApiKeyInEnv() {
|
|
2031
|
+
return DASHBOARD_API_KEY_ENVS.some((n) => !!process.env[n]?.trim());
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/publish-info.ts
|
|
2035
|
+
var PUBLISH_PACKAGE_URL = "https://www.npmjs.com/package/@pretense/cli";
|
|
2036
|
+
|
|
2037
|
+
// src/backend-client.ts
|
|
2038
|
+
function usageSummaryFromValidate(v) {
|
|
2039
|
+
return {
|
|
2040
|
+
plan: v.plan,
|
|
2041
|
+
monthlyMutationLimit: v.monthlyMutationLimit,
|
|
2042
|
+
mutationsUsed: v.mutationsUsed,
|
|
2043
|
+
mutationsRemaining: v.mutationsRemaining,
|
|
2044
|
+
requestsThisPeriod: 0,
|
|
2045
|
+
secretsBlockedThisPeriod: 0,
|
|
2046
|
+
periodResetsAt: v.periodResetsAt,
|
|
2047
|
+
keyExpiresAt: null,
|
|
2048
|
+
keyStatus: "active"
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
|
|
2052
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
2053
|
+
function getConfiguredApiRoot(rootOverride) {
|
|
2054
|
+
const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
|
|
2055
|
+
if (!raw) {
|
|
2056
|
+
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2057
|
+
}
|
|
2058
|
+
let normalized = raw;
|
|
2059
|
+
if (!/^https?:\/\//i.test(normalized)) {
|
|
2060
|
+
normalized = `https://${normalized}`;
|
|
2061
|
+
}
|
|
2062
|
+
try {
|
|
2063
|
+
const u = new URL(normalized);
|
|
2064
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
|
|
2065
|
+
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
2066
|
+
return `${origin}${path}`;
|
|
2067
|
+
} catch {
|
|
2068
|
+
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function getCliApiUrl(pathWithinCli, rootOverride) {
|
|
2072
|
+
const root = getConfiguredApiRoot(rootOverride);
|
|
2073
|
+
const tail = pathWithinCli.replace(/^\/+/, "");
|
|
2074
|
+
return `${root}/api/cli/${tail}`;
|
|
2075
|
+
}
|
|
2076
|
+
function getApiRequestTimeoutMs() {
|
|
2077
|
+
const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2078
|
+
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2079
|
+
const n = parseInt(raw, 10);
|
|
2080
|
+
return Math.min(Math.max(n, 3e3), 12e4);
|
|
2081
|
+
}
|
|
2082
|
+
function getApiKey() {
|
|
2083
|
+
const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
|
|
2084
|
+
if (fromEnv) return fromEnv;
|
|
2085
|
+
const fromFile = loadApiKey();
|
|
2086
|
+
if (!fromFile) return null;
|
|
2087
|
+
const t = fromFile.trim();
|
|
2088
|
+
return t.length > 0 ? t : null;
|
|
2089
|
+
}
|
|
2090
|
+
function getDashboardApiKey() {
|
|
2091
|
+
return getApiKey();
|
|
2092
|
+
}
|
|
2093
|
+
var cachedValidation = null;
|
|
2094
|
+
var lastValidationSuccessAt = 0;
|
|
2095
|
+
var VALIDATION_CACHE_FRESH_MS = 45e3;
|
|
2096
|
+
function getCachedValidation() {
|
|
2097
|
+
return cachedValidation;
|
|
2098
|
+
}
|
|
2099
|
+
function clearValidationCache() {
|
|
2100
|
+
cachedValidation = null;
|
|
2101
|
+
lastValidationSuccessAt = 0;
|
|
2102
|
+
}
|
|
2103
|
+
function bumpCachedUsageAfterLog(identifiersMutated) {
|
|
2104
|
+
if (!cachedValidation || identifiersMutated <= 0) return;
|
|
2105
|
+
cachedValidation.mutationsUsed += identifiersMutated;
|
|
2106
|
+
if (cachedValidation.mutationsRemaining !== -1) {
|
|
2107
|
+
cachedValidation.mutationsRemaining = Math.max(
|
|
2108
|
+
0,
|
|
2109
|
+
cachedValidation.mutationsRemaining - identifiersMutated
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function syncCachedUsageFromServer(codeProtectionsUsed, limit) {
|
|
2114
|
+
if (!cachedValidation) return;
|
|
2115
|
+
if (codeProtectionsUsed < cachedValidation.mutationsUsed) return;
|
|
2116
|
+
cachedValidation.mutationsUsed = codeProtectionsUsed;
|
|
2117
|
+
cachedValidation.monthlyMutationLimit = limit;
|
|
2118
|
+
if (limit !== -1) {
|
|
2119
|
+
cachedValidation.mutationsRemaining = Math.max(0, limit - codeProtectionsUsed);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
function makeBackendReachabilityError(code, message) {
|
|
2123
|
+
const err = new Error(message);
|
|
2124
|
+
err.code = code;
|
|
2125
|
+
return err;
|
|
2126
|
+
}
|
|
2127
|
+
async function validateKey(opts = {}) {
|
|
2128
|
+
const enforceReachability = opts.enforceReachability === true;
|
|
2129
|
+
const now = Date.now();
|
|
2130
|
+
const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
|
|
2131
|
+
if (!opts.force && cacheFresh) {
|
|
2132
|
+
return cachedValidation;
|
|
2133
|
+
}
|
|
2134
|
+
const validateUrl = getCliApiUrl("auth/validate");
|
|
2135
|
+
const timeoutMs = getApiRequestTimeoutMs();
|
|
2136
|
+
const apiKey = getApiKey();
|
|
2137
|
+
if (!apiKey) {
|
|
2138
|
+
if (enforceReachability) {
|
|
2139
|
+
const err = new Error("Missing dashboard API key");
|
|
2140
|
+
err.code = "MISSING_API_KEY";
|
|
2141
|
+
cachedValidation = null;
|
|
2142
|
+
lastValidationSuccessAt = 0;
|
|
2143
|
+
throw err;
|
|
2144
|
+
}
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
const controller = new AbortController();
|
|
2148
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2149
|
+
try {
|
|
2150
|
+
const resp = await fetch(validateUrl, {
|
|
2151
|
+
method: "POST",
|
|
2152
|
+
headers: {
|
|
2153
|
+
"content-type": "application/json",
|
|
2154
|
+
authorization: `Bearer ${apiKey}`
|
|
2155
|
+
},
|
|
2156
|
+
body: "{}",
|
|
2157
|
+
signal: controller.signal
|
|
2158
|
+
});
|
|
2159
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
2160
|
+
const raw2 = await resp.json().catch(() => ({}));
|
|
2161
|
+
const msg = typeof raw2.message === "string" ? raw2.message : "API key rejected by server";
|
|
2162
|
+
const err = new Error(msg);
|
|
2163
|
+
err.code = typeof raw2.code === "string" ? raw2.code : "INVALID_API_KEY";
|
|
2164
|
+
cachedValidation = null;
|
|
2165
|
+
lastValidationSuccessAt = 0;
|
|
2166
|
+
throw err;
|
|
2167
|
+
}
|
|
2168
|
+
if (!resp.ok) {
|
|
2169
|
+
const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
|
|
2170
|
+
if (enforceReachability) {
|
|
2171
|
+
cachedValidation = null;
|
|
2172
|
+
lastValidationSuccessAt = 0;
|
|
2173
|
+
throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
|
|
2174
|
+
}
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
let raw;
|
|
2178
|
+
try {
|
|
2179
|
+
raw = await resp.json();
|
|
2180
|
+
} catch {
|
|
2181
|
+
if (enforceReachability) {
|
|
2182
|
+
cachedValidation = null;
|
|
2183
|
+
lastValidationSuccessAt = 0;
|
|
2184
|
+
throw makeBackendReachabilityError(
|
|
2185
|
+
"AUTH_BACKEND_REJECTED",
|
|
2186
|
+
`Invalid JSON from ${validateUrl}`
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
const data = {
|
|
2192
|
+
...raw,
|
|
2193
|
+
mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
|
|
2194
|
+
mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
|
|
2195
|
+
monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
|
|
2196
|
+
};
|
|
2197
|
+
if (typeof data.valid === "boolean" && data.valid === false) {
|
|
2198
|
+
const err = new Error("API key is not valid");
|
|
2199
|
+
err.code = "INVALID_API_KEY";
|
|
2200
|
+
cachedValidation = null;
|
|
2201
|
+
lastValidationSuccessAt = 0;
|
|
2202
|
+
throw err;
|
|
2203
|
+
}
|
|
2204
|
+
cachedValidation = data;
|
|
2205
|
+
lastValidationSuccessAt = Date.now();
|
|
2206
|
+
return data;
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
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") {
|
|
2209
|
+
throw err;
|
|
2210
|
+
}
|
|
2211
|
+
if (enforceReachability) {
|
|
2212
|
+
const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS or PRETEST_API_TIMEOUT_MS)` : `Cannot reach dashboard API at ${validateUrl}: ${err.message}`;
|
|
2213
|
+
cachedValidation = null;
|
|
2214
|
+
lastValidationSuccessAt = 0;
|
|
2215
|
+
throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
|
|
2216
|
+
}
|
|
2217
|
+
if (opts.verbose) {
|
|
2218
|
+
process.stderr.write(
|
|
2219
|
+
`[PRETENSE] Backend validation failed: ${err.message}
|
|
2220
|
+
`
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
return null;
|
|
2224
|
+
} finally {
|
|
2225
|
+
clearTimeout(timer);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
async function fetchUsageSummary(opts = {}) {
|
|
2229
|
+
const summaryUrl = getCliApiUrl("usage/summary");
|
|
2230
|
+
const apiKey = getApiKey();
|
|
2231
|
+
if (!apiKey) return null;
|
|
2232
|
+
const controller = new AbortController();
|
|
2233
|
+
const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
|
|
2234
|
+
try {
|
|
2235
|
+
const resp = await fetch(summaryUrl, {
|
|
2236
|
+
method: "GET",
|
|
2237
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
2238
|
+
signal: controller.signal
|
|
2239
|
+
});
|
|
2240
|
+
if (!resp.ok) return null;
|
|
2241
|
+
const raw = await resp.json();
|
|
2242
|
+
return {
|
|
2243
|
+
...raw,
|
|
2244
|
+
mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
|
|
2245
|
+
mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
|
|
2246
|
+
monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
|
|
2247
|
+
};
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
if (opts.verbose) {
|
|
2250
|
+
process.stderr.write(
|
|
2251
|
+
`[PRETENSE] Usage summary fetch failed: ${err.message}
|
|
2252
|
+
`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
return null;
|
|
2256
|
+
} finally {
|
|
2257
|
+
clearTimeout(timer);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
async function isWithinLimits() {
|
|
2261
|
+
const v = getCachedValidation();
|
|
2262
|
+
if (!v) return { allowed: true };
|
|
2263
|
+
if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
|
|
2264
|
+
const fresh = await validateKey({ force: true }).catch(() => null);
|
|
2265
|
+
const check = fresh ?? v;
|
|
2266
|
+
if (check.mutationsRemaining !== -1 && check.mutationsRemaining <= 0) {
|
|
2267
|
+
return {
|
|
2268
|
+
allowed: false,
|
|
2269
|
+
reason: `Monthly security token limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
return { allowed: true };
|
|
2273
|
+
}
|
|
2274
|
+
return { allowed: true };
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/log-uploader.ts
|
|
2278
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
2279
|
+
var buffer = [];
|
|
2280
|
+
var flushTimer = null;
|
|
2281
|
+
function startFlushTimer(verbose) {
|
|
2282
|
+
if (flushTimer !== null) return;
|
|
2283
|
+
flushTimer = setInterval(() => {
|
|
2284
|
+
void flushBuffer(verbose);
|
|
2285
|
+
}, FLUSH_INTERVAL_MS);
|
|
2286
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
2287
|
+
}
|
|
2288
|
+
function attachExitFlush() {
|
|
2289
|
+
const flush = () => {
|
|
2290
|
+
void flushBuffer(false);
|
|
2291
|
+
};
|
|
2292
|
+
process.once("exit", flush);
|
|
2293
|
+
process.once("SIGINT", () => {
|
|
2294
|
+
flush();
|
|
2295
|
+
process.exit(0);
|
|
2296
|
+
});
|
|
2297
|
+
process.once("SIGTERM", () => {
|
|
2298
|
+
flush();
|
|
2299
|
+
process.exit(0);
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
var exitFlushAttached = false;
|
|
2303
|
+
async function flushBuffer(verbose) {
|
|
2304
|
+
if (buffer.length === 0) return;
|
|
2305
|
+
const snapshot = buffer;
|
|
2306
|
+
buffer = [];
|
|
2307
|
+
const last = snapshot[snapshot.length - 1];
|
|
2308
|
+
const { options } = last;
|
|
2309
|
+
let file_count = 0;
|
|
2310
|
+
let identifiers_mutated = 0;
|
|
2311
|
+
let secrets_blocked = 0;
|
|
2312
|
+
let scan_tokens = 0;
|
|
2313
|
+
let mutation_tokens = 0;
|
|
2314
|
+
let read_tokens = 0;
|
|
2315
|
+
let llm_provider = "unknown";
|
|
2316
|
+
let cli_version;
|
|
2317
|
+
for (const { event } of snapshot) {
|
|
2318
|
+
file_count += event.file_count ?? 0;
|
|
2319
|
+
identifiers_mutated += event.identifiers_mutated ?? 0;
|
|
2320
|
+
secrets_blocked += event.secrets_blocked ?? 0;
|
|
2321
|
+
scan_tokens += event.scan_tokens ?? 0;
|
|
2322
|
+
mutation_tokens += event.mutation_tokens ?? 0;
|
|
2323
|
+
read_tokens += event.read_tokens ?? 0;
|
|
2324
|
+
if (event.llm_provider) llm_provider = event.llm_provider;
|
|
2325
|
+
if (event.cli_version) cli_version = event.cli_version;
|
|
2326
|
+
}
|
|
2327
|
+
const apiRoot = options.apiUrl?.trim() || void 0;
|
|
2328
|
+
const endpoint = getCliApiUrl("log", apiRoot);
|
|
2329
|
+
const { apiKey, timeoutMs = getApiRequestTimeoutMs(), onResponse } = options;
|
|
2330
|
+
if (!apiKey) return;
|
|
2331
|
+
const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
|
|
2332
|
+
const idempotency_key = crypto.randomUUID();
|
|
2333
|
+
const body = {
|
|
2334
|
+
file_count,
|
|
2335
|
+
identifiers_mutated,
|
|
2336
|
+
identifiers_code_protected: identifiers_mutated,
|
|
2337
|
+
secrets_blocked,
|
|
2338
|
+
llm_provider,
|
|
2339
|
+
scan_tokens,
|
|
2340
|
+
code_protection_tokens: mutation_tokens,
|
|
2341
|
+
read_tokens,
|
|
2342
|
+
idempotency_key,
|
|
2343
|
+
...cli_version ? { cli_version } : {},
|
|
2344
|
+
git_remote: gitCtx.git_remote,
|
|
2345
|
+
repo_remote_url: gitCtx.git_remote,
|
|
2346
|
+
git_branch: gitCtx.git_branch,
|
|
2347
|
+
commit_sha: gitCtx.git_commit_sha,
|
|
2348
|
+
git_commit_sha: gitCtx.git_commit_sha
|
|
2349
|
+
};
|
|
2350
|
+
const controller = new AbortController();
|
|
2351
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2352
|
+
try {
|
|
2353
|
+
const resp = await fetch(endpoint, {
|
|
2354
|
+
method: "POST",
|
|
2355
|
+
headers: {
|
|
2356
|
+
"content-type": "application/json",
|
|
2357
|
+
authorization: `Bearer ${apiKey}`
|
|
2358
|
+
},
|
|
2359
|
+
body: JSON.stringify(body),
|
|
2360
|
+
signal: controller.signal
|
|
2361
|
+
});
|
|
2362
|
+
if (resp.ok) {
|
|
2363
|
+
const data = await resp.json().catch(() => null);
|
|
2364
|
+
if (data && typeof data.codeProtectionsUsed === "number" && typeof data.limit === "number") {
|
|
2365
|
+
onResponse?.(data.codeProtectionsUsed, data.limit);
|
|
2366
|
+
}
|
|
2367
|
+
} else if (verbose) {
|
|
2368
|
+
const detail = await resp.text().catch(() => "");
|
|
2369
|
+
process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status} (batch of ${snapshot.length}): ${detail}
|
|
2370
|
+
`);
|
|
2371
|
+
}
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
if (verbose) {
|
|
2374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2375
|
+
process.stderr.write(`[PRETENSE] log upload failed: ${msg}
|
|
2376
|
+
`);
|
|
2377
|
+
}
|
|
2378
|
+
} finally {
|
|
2379
|
+
clearTimeout(timer);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async function uploadLog(event, options) {
|
|
2383
|
+
if (!options.apiKey) return;
|
|
2384
|
+
buffer.push({ event, options });
|
|
2385
|
+
startFlushTimer(options.verbose ?? false);
|
|
2386
|
+
if (!exitFlushAttached) {
|
|
2387
|
+
exitFlushAttached = true;
|
|
2388
|
+
attachExitFlush();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/start-client-instructions.ts
|
|
2393
|
+
import chalk2 from "chalk";
|
|
2394
|
+
function printStartClientInstructions(port) {
|
|
2395
|
+
const host = `http://localhost:${port}`;
|
|
2396
|
+
console.log();
|
|
2397
|
+
console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
|
|
2398
|
+
console.log(chalk2.gray("Point the CLI at the Pretense backend API (optional if using default):"));
|
|
2399
|
+
console.log(
|
|
2400
|
+
chalk2.white(`export PRETENSE_API_URL='https://api.pretense.ai'`)
|
|
2401
|
+
);
|
|
2402
|
+
console.log();
|
|
2403
|
+
console.log(chalk2.gray("Log in with your Pretense dashboard API key:"));
|
|
2404
|
+
console.log(chalk2.white("pretense login --key your-pretense-api-key"));
|
|
2405
|
+
console.log();
|
|
2406
|
+
console.log(
|
|
2407
|
+
chalk2.gray("Set the LLM base URL for your provider (proxy below):")
|
|
2408
|
+
);
|
|
2409
|
+
console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
|
|
2410
|
+
console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
|
|
2411
|
+
console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
|
|
2412
|
+
console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
|
|
2413
|
+
console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
|
|
2414
|
+
console.log();
|
|
2415
|
+
console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
|
|
2416
|
+
console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
|
|
2417
|
+
console.log();
|
|
2418
|
+
console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
|
|
2419
|
+
console.log(
|
|
2420
|
+
chalk2.gray(
|
|
2421
|
+
"Stop the proxy: press Ctrl+C in the terminal where pretense start is running."
|
|
2422
|
+
)
|
|
2423
|
+
);
|
|
2424
|
+
console.log(
|
|
2425
|
+
chalk2.gray(
|
|
2426
|
+
"Remove the saved Pretense dashboard key from ~/.pretense/config.json:"
|
|
2427
|
+
)
|
|
2428
|
+
);
|
|
2429
|
+
console.log(chalk2.white("pretense logout"));
|
|
2430
|
+
console.log(
|
|
2431
|
+
chalk2.gray(
|
|
2432
|
+
"(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
|
|
2433
|
+
)
|
|
2434
|
+
);
|
|
2435
|
+
console.log(
|
|
2436
|
+
chalk2.gray(
|
|
2437
|
+
"PRETENSE_API_KEY set in that same terminal \u2014 then press Ctrl+C and run pretense start again."
|
|
2438
|
+
)
|
|
2439
|
+
);
|
|
2440
|
+
console.log();
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/version.ts
|
|
2444
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2445
|
+
import { fileURLToPath } from "url";
|
|
2446
|
+
var cached = null;
|
|
2447
|
+
function getCliSemver() {
|
|
2448
|
+
if (cached !== null) return cached;
|
|
2449
|
+
if (typeof __BINARY_VERSION__ !== "undefined" && __BINARY_VERSION__) {
|
|
2450
|
+
cached = __BINARY_VERSION__;
|
|
2451
|
+
return cached;
|
|
2452
|
+
}
|
|
2453
|
+
try {
|
|
2454
|
+
const url = new URL("../package.json", import.meta.url);
|
|
2455
|
+
const raw = readFileSync6(fileURLToPath(url), "utf-8");
|
|
2456
|
+
cached = JSON.parse(raw).version ?? "0.0.0";
|
|
2457
|
+
} catch {
|
|
2458
|
+
cached = "0.0.0";
|
|
2459
|
+
}
|
|
2460
|
+
return cached;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/proxy.ts
|
|
2464
|
+
var dashboardSavedCredentialsRevoked = false;
|
|
2465
|
+
function attachDashboardLogoutWatcher(enabled) {
|
|
2466
|
+
if (!enabled) return;
|
|
2467
|
+
const dir = getUserDashboardConfigDir();
|
|
2468
|
+
let debounceTimer;
|
|
2469
|
+
const applyFilesystemCredentialState = () => {
|
|
2470
|
+
const fk = loadApiKey();
|
|
2471
|
+
if (!fk) {
|
|
2472
|
+
if (dashboardSavedCredentialsRevoked) return;
|
|
2473
|
+
dashboardSavedCredentialsRevoked = true;
|
|
2474
|
+
clearDashboardApiKeyFromProcess();
|
|
2475
|
+
clearValidationCache();
|
|
2476
|
+
try {
|
|
2477
|
+
process.stderr.write(
|
|
2478
|
+
`\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
|
|
2479
|
+
`
|
|
2480
|
+
);
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
if (dashboardSavedCredentialsRevoked) {
|
|
2486
|
+
dashboardSavedCredentialsRevoked = false;
|
|
2487
|
+
clearValidationCache();
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
const schedule = () => {
|
|
2491
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2492
|
+
debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
|
|
2493
|
+
};
|
|
2494
|
+
try {
|
|
2495
|
+
if (!existsSync6(dir)) return;
|
|
2496
|
+
watch(dir, () => {
|
|
2497
|
+
schedule();
|
|
2498
|
+
});
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
function anthropicUpstream() {
|
|
2503
|
+
return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
|
|
2504
|
+
}
|
|
2505
|
+
function openaiUpstream() {
|
|
2506
|
+
return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
|
|
2507
|
+
}
|
|
2508
|
+
function geminiUpstream() {
|
|
2509
|
+
return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
|
|
2510
|
+
}
|
|
2511
|
+
function ollamaUpstream() {
|
|
2512
|
+
return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
|
|
2513
|
+
}
|
|
2514
|
+
function detectUpstream(path) {
|
|
2515
|
+
if (path.startsWith("/v1/messages")) return anthropicUpstream();
|
|
2516
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
2517
|
+
return openaiUpstream();
|
|
2518
|
+
}
|
|
2519
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
|
|
2520
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
|
|
2521
|
+
return anthropicUpstream();
|
|
2522
|
+
}
|
|
2523
|
+
function detectProvider(path) {
|
|
2524
|
+
if (path.startsWith("/v1/messages")) return "anthropic";
|
|
2525
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
2526
|
+
return "openai";
|
|
2527
|
+
}
|
|
2528
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
|
|
2529
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
|
|
2530
|
+
return "anthropic";
|
|
2531
|
+
}
|
|
2532
|
+
function stripPretenseOnlyForwardingHeaders(headers) {
|
|
2533
|
+
const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
|
|
2534
|
+
for (const k of [...Object.keys(headers)]) {
|
|
2535
|
+
if (drop.has(k.toLowerCase())) delete headers[k];
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
function extractText(body) {
|
|
2539
|
+
const parts = [];
|
|
2540
|
+
if (typeof body["system"] === "string") parts.push(body["system"]);
|
|
2541
|
+
if (Array.isArray(body["messages"])) {
|
|
2542
|
+
for (const msg of body["messages"]) {
|
|
2543
|
+
if (typeof msg["content"] === "string") parts.push(msg["content"]);
|
|
2544
|
+
else if (Array.isArray(msg["content"])) {
|
|
2545
|
+
for (const block of msg["content"]) {
|
|
2546
|
+
if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return parts.join("\n");
|
|
2552
|
+
}
|
|
2553
|
+
function extractCodeBlocks(text) {
|
|
2554
|
+
const blocks = [];
|
|
2555
|
+
const re = /```(\w*)\n([\s\S]*?)```/g;
|
|
2556
|
+
let m;
|
|
2557
|
+
while ((m = re.exec(text)) !== null) {
|
|
2558
|
+
blocks.push({
|
|
2559
|
+
code: m[2] ?? "",
|
|
2560
|
+
language: m[1] ?? "typescript",
|
|
2561
|
+
start: m.index,
|
|
2562
|
+
end: m.index + m[0].length
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
return blocks;
|
|
2566
|
+
}
|
|
2567
|
+
function replaceCodeBlocks(text, blocks, replacements) {
|
|
2568
|
+
let result = text;
|
|
2569
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
2570
|
+
const block = blocks[i];
|
|
2571
|
+
const replacement = replacements[i] ?? block.code;
|
|
2572
|
+
const lang = block.language ?? "typescript";
|
|
2573
|
+
result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
|
|
2574
|
+
}
|
|
2575
|
+
return result;
|
|
2576
|
+
}
|
|
2577
|
+
function applyToBody(body, transform) {
|
|
2578
|
+
const mutateField = (val) => {
|
|
2579
|
+
if (typeof val === "string") return transform(val);
|
|
2580
|
+
if (Array.isArray(val)) return val.map(mutateField);
|
|
2581
|
+
if (val && typeof val === "object") {
|
|
2582
|
+
const obj = val;
|
|
2583
|
+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
|
|
2584
|
+
}
|
|
2585
|
+
return val;
|
|
2586
|
+
};
|
|
2587
|
+
return mutateField(body);
|
|
2588
|
+
}
|
|
2589
|
+
var stats = {
|
|
2590
|
+
requestsProcessed: 0,
|
|
2591
|
+
totalTokensMutated: 0,
|
|
2592
|
+
totalSecretsBlocked: 0,
|
|
2593
|
+
startedAt: Date.now()
|
|
2594
|
+
};
|
|
2595
|
+
function daysSinceFirstUse() {
|
|
2596
|
+
const usage = loadUsage();
|
|
2597
|
+
if (!usage.firstUseDate) return 0;
|
|
2598
|
+
const first = new Date(usage.firstUseDate).getTime();
|
|
2599
|
+
return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
|
|
2600
|
+
}
|
|
2601
|
+
function printUpgradeNudge(reason) {
|
|
2602
|
+
process.stdout.write(
|
|
2603
|
+
`
|
|
2604
|
+
\x1B[33m[PRETENSE] ${reason}\x1B[0m
|
|
2605
|
+
\x1B[33m[PRETENSE] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2606
|
+
|
|
2607
|
+
`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
function createProxy(opts = {}) {
|
|
2611
|
+
const config = { ...DEFAULT_CONFIG, ...opts.config };
|
|
1683
2612
|
const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
|
|
1684
2613
|
const verbose = opts.verbose ?? false;
|
|
1685
2614
|
const app = new Hono();
|
|
@@ -1688,7 +2617,7 @@ function createProxy(opts = {}) {
|
|
|
1688
2617
|
"/",
|
|
1689
2618
|
(c) => c.json({
|
|
1690
2619
|
name: "pretense",
|
|
1691
|
-
version:
|
|
2620
|
+
version: getCliSemver(),
|
|
1692
2621
|
status: "running",
|
|
1693
2622
|
routes: {
|
|
1694
2623
|
"GET /": "This welcome page",
|
|
@@ -1698,14 +2627,14 @@ function createProxy(opts = {}) {
|
|
|
1698
2627
|
"POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
|
|
1699
2628
|
},
|
|
1700
2629
|
languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
|
|
1701
|
-
docs:
|
|
2630
|
+
docs: PUBLISH_PACKAGE_URL
|
|
1702
2631
|
})
|
|
1703
2632
|
);
|
|
1704
2633
|
app.get(
|
|
1705
2634
|
"/health",
|
|
1706
2635
|
(c) => c.json({
|
|
1707
2636
|
status: "ok",
|
|
1708
|
-
version:
|
|
2637
|
+
version: getCliSemver(),
|
|
1709
2638
|
uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
|
|
1710
2639
|
stats: {
|
|
1711
2640
|
requestsProcessed: stats.requestsProcessed,
|
|
@@ -1735,6 +2664,7 @@ function createProxy(opts = {}) {
|
|
|
1735
2664
|
});
|
|
1736
2665
|
delete headers2["host"];
|
|
1737
2666
|
headers2["host"] = new URL(upstream2).host;
|
|
2667
|
+
stripPretenseOnlyForwardingHeaders(headers2);
|
|
1738
2668
|
const resp = await fetch(url, { method: c.req.method, headers: headers2 });
|
|
1739
2669
|
return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
|
|
1740
2670
|
}
|
|
@@ -1751,22 +2681,93 @@ function createProxy(opts = {}) {
|
|
|
1751
2681
|
const requestId = nanoid(12);
|
|
1752
2682
|
const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1753
2683
|
const provider = detectProvider(c.req.path);
|
|
2684
|
+
if (dashboardSavedCredentialsRevoked) {
|
|
2685
|
+
return c.json(
|
|
2686
|
+
{
|
|
2687
|
+
error: {
|
|
2688
|
+
type: "pretense_session_logged_out",
|
|
2689
|
+
message: "Saved dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with a dashboard API key in this shell, stop it (Ctrl+C) and start again after logout."
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
401
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
const dashboardApiKey = getDashboardApiKey();
|
|
2696
|
+
if (!dashboardApiKey) {
|
|
2697
|
+
return c.json(
|
|
2698
|
+
{
|
|
2699
|
+
error: {
|
|
2700
|
+
type: "pretense_dashboard_key_required",
|
|
2701
|
+
message: "Configure your dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
2702
|
+
}
|
|
2703
|
+
},
|
|
2704
|
+
401
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
try {
|
|
2708
|
+
await validateKey({ verbose, force: false, enforceReachability: true });
|
|
2709
|
+
} catch (err) {
|
|
2710
|
+
const code = err.code ?? "";
|
|
2711
|
+
const msg = err.message;
|
|
2712
|
+
const knownAuthCodes = /* @__PURE__ */ new Set([
|
|
2713
|
+
"KEY_REVOKED",
|
|
2714
|
+
"KEY_EXPIRED",
|
|
2715
|
+
"INVALID_API_KEY",
|
|
2716
|
+
"MISSING_API_KEY"
|
|
2717
|
+
]);
|
|
2718
|
+
if (knownAuthCodes.has(code)) {
|
|
2719
|
+
return c.json(
|
|
2720
|
+
{ error: { type: "pretense_auth_error", code, message: msg } },
|
|
2721
|
+
401
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
2725
|
+
return c.json(
|
|
2726
|
+
{ error: { type: "pretense_auth_backend_error", code, message: msg } },
|
|
2727
|
+
503
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
if (code === "ORG_INACTIVE") {
|
|
2731
|
+
return c.json(
|
|
2732
|
+
{ error: { type: "pretense_org_inactive", code, message: msg } },
|
|
2733
|
+
403
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
return c.json(
|
|
2737
|
+
{ error: { type: "pretense_auth_error", message: msg } },
|
|
2738
|
+
401
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
1754
2741
|
const sessHash = c.get("sessionHash");
|
|
1755
2742
|
const session = getSession(sessHash);
|
|
1756
|
-
const tier =
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2743
|
+
const tier = tierFromDashboardPlan(
|
|
2744
|
+
getCachedValidation()?.plan,
|
|
2745
|
+
detectTier()
|
|
2746
|
+
);
|
|
2747
|
+
const backendLimits = await isWithinLimits();
|
|
2748
|
+
if (!backendLimits.allowed) {
|
|
1760
2749
|
return c.json(
|
|
1761
2750
|
{
|
|
1762
2751
|
error: {
|
|
1763
2752
|
type: "pretense_rate_limit",
|
|
1764
|
-
message:
|
|
2753
|
+
message: backendLimits.reason ?? "Security token limit exceeded."
|
|
1765
2754
|
}
|
|
1766
2755
|
},
|
|
1767
2756
|
429
|
|
1768
2757
|
);
|
|
1769
2758
|
}
|
|
2759
|
+
const cliAuth = getCachedValidation();
|
|
2760
|
+
if (cliAuth?.permission === "read_only") {
|
|
2761
|
+
return c.json(
|
|
2762
|
+
{
|
|
2763
|
+
error: {
|
|
2764
|
+
type: "pretense_forbidden",
|
|
2765
|
+
message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretense proxy."
|
|
2766
|
+
}
|
|
2767
|
+
},
|
|
2768
|
+
403
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
1770
2771
|
const fullText = extractText(body);
|
|
1771
2772
|
const secretScan = scan2(fullText);
|
|
1772
2773
|
const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
|
|
@@ -1790,7 +2791,17 @@ function createProxy(opts = {}) {
|
|
|
1790
2791
|
let tokensMutated = 0;
|
|
1791
2792
|
const mutatedBody = applyToBody(processedBody, (text) => {
|
|
1792
2793
|
const blocks = extractCodeBlocks(text);
|
|
1793
|
-
if (blocks.length === 0)
|
|
2794
|
+
if (blocks.length === 0) {
|
|
2795
|
+
const lang = detectLanguage(text);
|
|
2796
|
+
const result = mutate(text, lang);
|
|
2797
|
+
for (const [k, v] of result.map) {
|
|
2798
|
+
mutationMap.set(k, v);
|
|
2799
|
+
session.mutationMap.set(k, v);
|
|
2800
|
+
session.reverseMap.set(v, k);
|
|
2801
|
+
}
|
|
2802
|
+
tokensMutated += result.stats.tokensMutated;
|
|
2803
|
+
return result.mutatedCode;
|
|
2804
|
+
}
|
|
1794
2805
|
const mutatedBlocks = [];
|
|
1795
2806
|
for (const block of blocks) {
|
|
1796
2807
|
const result = mutate(block.code, block.language);
|
|
@@ -1804,14 +2815,37 @@ function createProxy(opts = {}) {
|
|
|
1804
2815
|
}
|
|
1805
2816
|
return replaceCodeBlocks(text, blocks, mutatedBlocks);
|
|
1806
2817
|
});
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2818
|
+
const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
|
|
2819
|
+
if (dashboardApiKey && !remoteLogDisabled) {
|
|
2820
|
+
bumpCachedUsageAfterLog(tokensMutated);
|
|
2821
|
+
void uploadLog(
|
|
2822
|
+
{
|
|
2823
|
+
file_count: 0,
|
|
2824
|
+
identifiers_mutated: tokensMutated,
|
|
2825
|
+
secrets_blocked: blockedSecrets.length,
|
|
2826
|
+
llm_provider: provider,
|
|
2827
|
+
cli_version: getCliSemver()
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
apiKey: dashboardApiKey,
|
|
2831
|
+
verbose,
|
|
2832
|
+
onResponse: (codeProtectionsUsed, limit) => {
|
|
2833
|
+
syncCachedUsageFromServer(codeProtectionsUsed, limit);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
const warnCache = getCachedValidation();
|
|
2839
|
+
if (warnCache && warnCache.monthlyMutationLimit !== -1 && tier === "free") {
|
|
2840
|
+
const limit = warnCache.monthlyMutationLimit;
|
|
2841
|
+
const used = warnCache.mutationsUsed;
|
|
2842
|
+
const warningThreshold = Math.floor(limit * 0.8);
|
|
2843
|
+
if (used >= warningThreshold && used - tokensMutated < warningThreshold) {
|
|
2844
|
+
printUpgradeNudge(`${used}/${limit} monthly security tokens used. Running low!`);
|
|
2845
|
+
}
|
|
1812
2846
|
}
|
|
1813
2847
|
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
1814
|
-
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited
|
|
2848
|
+
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited security tokens, 90-day audit, Slack alerts.");
|
|
1815
2849
|
}
|
|
1816
2850
|
const entry = {
|
|
1817
2851
|
id: requestId,
|
|
@@ -1828,7 +2862,7 @@ function createProxy(opts = {}) {
|
|
|
1828
2862
|
createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
|
|
1829
2863
|
);
|
|
1830
2864
|
if (verbose && tokensMutated > 0) {
|
|
1831
|
-
process.stdout.write(`[PRETENSE] ${tokensMutated} tokens
|
|
2865
|
+
process.stdout.write(`[PRETENSE] ${tokensMutated} security tokens protected for request ${requestId}
|
|
1832
2866
|
`);
|
|
1833
2867
|
}
|
|
1834
2868
|
const headers = {};
|
|
@@ -1839,6 +2873,7 @@ function createProxy(opts = {}) {
|
|
|
1839
2873
|
delete headers["content-length"];
|
|
1840
2874
|
headers["host"] = new URL(upstream).host;
|
|
1841
2875
|
headers["x-pretense-request-id"] = requestId;
|
|
2876
|
+
stripPretenseOnlyForwardingHeaders(headers);
|
|
1842
2877
|
const forwardBody = JSON.stringify(mutatedBody);
|
|
1843
2878
|
let upstreamResp;
|
|
1844
2879
|
try {
|
|
@@ -1867,12 +2902,12 @@ function createProxy(opts = {}) {
|
|
|
1867
2902
|
}
|
|
1868
2903
|
const decoder = new TextDecoder();
|
|
1869
2904
|
const encoder = new TextEncoder();
|
|
1870
|
-
let
|
|
2905
|
+
let buffer2 = "";
|
|
1871
2906
|
const transform = new TransformStream({
|
|
1872
2907
|
transform(chunk, controller) {
|
|
1873
|
-
|
|
1874
|
-
const lines =
|
|
1875
|
-
|
|
2908
|
+
buffer2 += decoder.decode(chunk, { stream: true });
|
|
2909
|
+
const lines = buffer2.split("\n");
|
|
2910
|
+
buffer2 = lines.pop() ?? "";
|
|
1876
2911
|
for (const line of lines) {
|
|
1877
2912
|
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1878
2913
|
try {
|
|
@@ -1889,8 +2924,8 @@ function createProxy(opts = {}) {
|
|
|
1889
2924
|
}
|
|
1890
2925
|
},
|
|
1891
2926
|
flush(controller) {
|
|
1892
|
-
if (
|
|
1893
|
-
controller.enqueue(encoder.encode(
|
|
2927
|
+
if (buffer2.length > 0) {
|
|
2928
|
+
controller.enqueue(encoder.encode(buffer2));
|
|
1894
2929
|
}
|
|
1895
2930
|
}
|
|
1896
2931
|
});
|
|
@@ -1934,9 +2969,9 @@ function createProxy(opts = {}) {
|
|
|
1934
2969
|
return app;
|
|
1935
2970
|
}
|
|
1936
2971
|
function isPortAvailable(port) {
|
|
1937
|
-
return new Promise((
|
|
1938
|
-
const tester = createServer().once("error", () =>
|
|
1939
|
-
tester.once("close", () =>
|
|
2972
|
+
return new Promise((resolve4) => {
|
|
2973
|
+
const tester = createServer().once("error", () => resolve4(false)).once("listening", () => {
|
|
2974
|
+
tester.once("close", () => resolve4(true)).close();
|
|
1940
2975
|
}).listen(port, "127.0.0.1");
|
|
1941
2976
|
});
|
|
1942
2977
|
}
|
|
@@ -1953,8 +2988,103 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
1953
2988
|
function startProxy(opts = {}) {
|
|
1954
2989
|
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
1955
2990
|
const app = createProxy(opts);
|
|
1956
|
-
const tier = detectTier();
|
|
1957
2991
|
void (async () => {
|
|
2992
|
+
const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
|
|
2993
|
+
const dashboardKey = getDashboardApiKey();
|
|
2994
|
+
if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
|
|
2995
|
+
process.stderr.write(
|
|
2996
|
+
`\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
|
|
2997
|
+
\x1B[31m[PRETENSE] or set PRETENSE_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2998
|
+
`
|
|
2999
|
+
);
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3003
|
+
for (; ; ) {
|
|
3004
|
+
try {
|
|
3005
|
+
const validation = await validateKey({
|
|
3006
|
+
force: true,
|
|
3007
|
+
verbose: opts.verbose,
|
|
3008
|
+
enforceReachability: true
|
|
3009
|
+
});
|
|
3010
|
+
if (!validation) {
|
|
3011
|
+
process.stderr.write(
|
|
3012
|
+
`\x1B[31m[PRETENSE] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
|
|
3013
|
+
`
|
|
3014
|
+
);
|
|
3015
|
+
process.exit(1);
|
|
3016
|
+
}
|
|
3017
|
+
const plan = validation.plan ?? "FREE";
|
|
3018
|
+
const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
|
|
3019
|
+
process.stdout.write(
|
|
3020
|
+
`\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} security tokens remaining\x1B[0m
|
|
3021
|
+
`
|
|
3022
|
+
);
|
|
3023
|
+
break;
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
const code = err.code;
|
|
3026
|
+
const msg = err.message;
|
|
3027
|
+
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
3028
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
3029
|
+
`);
|
|
3030
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL (or PRETEST_API_URL) if using a custom API.\x1B[0m
|
|
3031
|
+
`);
|
|
3032
|
+
process.exit(1);
|
|
3033
|
+
}
|
|
3034
|
+
if (code === "ORG_INACTIVE") {
|
|
3035
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
3036
|
+
`);
|
|
3037
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Your organization is inactive; contact support.\x1B[0m
|
|
3038
|
+
`);
|
|
3039
|
+
process.exit(1);
|
|
3040
|
+
}
|
|
3041
|
+
const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
|
|
3042
|
+
if (authRetryable && interactive) {
|
|
3043
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
3044
|
+
`);
|
|
3045
|
+
process.stderr.write(
|
|
3046
|
+
`\x1B[33m[PRETENSE] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
|
|
3047
|
+
`
|
|
3048
|
+
);
|
|
3049
|
+
let entered = null;
|
|
3050
|
+
for (; ; ) {
|
|
3051
|
+
entered = await promptDashboardApiKeyAfterFailure();
|
|
3052
|
+
if (!entered) {
|
|
3053
|
+
process.stderr.write(`\x1B[31m[PRETENSE] No key entered; exiting.\x1B[0m
|
|
3054
|
+
`);
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
3057
|
+
if (isValidKeyShape(entered)) break;
|
|
3058
|
+
process.stderr.write(
|
|
3059
|
+
`\x1B[31m[PRETENSE] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
|
|
3060
|
+
`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
const savedPath = installDashboardKeyForCurrentProcess(entered);
|
|
3064
|
+
process.stdout.write(`\x1B[32m[PRETENSE] Saved API key to ${savedPath}\x1B[0m
|
|
3065
|
+
`);
|
|
3066
|
+
clearValidationCache();
|
|
3067
|
+
continue;
|
|
3068
|
+
}
|
|
3069
|
+
if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
|
|
3070
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
3071
|
+
`);
|
|
3072
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' to fix.\x1B[0m
|
|
3073
|
+
`);
|
|
3074
|
+
process.exit(1);
|
|
3075
|
+
}
|
|
3076
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
3077
|
+
`);
|
|
3078
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' or check connectivity.\x1B[0m
|
|
3079
|
+
`);
|
|
3080
|
+
process.exit(1);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
|
|
3084
|
+
const bannerTier = tierFromDashboardPlan(
|
|
3085
|
+
getCachedValidation()?.plan,
|
|
3086
|
+
detectTier()
|
|
3087
|
+
);
|
|
1958
3088
|
let port;
|
|
1959
3089
|
try {
|
|
1960
3090
|
port = await findAvailablePort(requestedPort, 10);
|
|
@@ -1969,10 +3099,11 @@ function startProxy(opts = {}) {
|
|
|
1969
3099
|
`
|
|
1970
3100
|
);
|
|
1971
3101
|
}
|
|
3102
|
+
printStartClientInstructions(port);
|
|
1972
3103
|
serve({ fetch: app.fetch, port }, () => {
|
|
1973
3104
|
const portStr = String(port);
|
|
1974
3105
|
process.stdout.write(`
|
|
1975
|
-
\x1B[36mPretense
|
|
3106
|
+
\x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
|
|
1976
3107
|
Your code hits AI naked. We fix that.
|
|
1977
3108
|
|
|
1978
3109
|
Proxy: http://localhost:${portStr}
|
|
@@ -1992,12 +3123,22 @@ function startProxy(opts = {}) {
|
|
|
1992
3123
|
/v1beta/... -> Google Gemini
|
|
1993
3124
|
/api/chat, /api/generate -> Ollama (local)
|
|
1994
3125
|
`);
|
|
3126
|
+
setInterval(() => {
|
|
3127
|
+
if (dashboardSavedCredentialsRevoked) return;
|
|
3128
|
+
if (!getDashboardApiKey()) return;
|
|
3129
|
+
void validateKey({
|
|
3130
|
+
force: true,
|
|
3131
|
+
verbose: opts.verbose,
|
|
3132
|
+
enforceReachability: true
|
|
3133
|
+
}).catch(() => {
|
|
3134
|
+
});
|
|
3135
|
+
}, 6e4);
|
|
1995
3136
|
});
|
|
1996
3137
|
})();
|
|
1997
3138
|
}
|
|
1998
3139
|
|
|
1999
3140
|
// src/token-footer.ts
|
|
2000
|
-
import
|
|
3141
|
+
import chalk3 from "chalk";
|
|
2001
3142
|
var PLAN_LIMITS = {
|
|
2002
3143
|
free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
|
|
2003
3144
|
pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
|
|
@@ -2008,45 +3149,49 @@ var PLAN_LIMITS = {
|
|
|
2008
3149
|
pricePerMonth: "$99/seat/month"
|
|
2009
3150
|
}
|
|
2010
3151
|
};
|
|
2011
|
-
function getPlanLimits(tier) {
|
|
2012
|
-
return PLAN_LIMITS[tier];
|
|
2013
|
-
}
|
|
2014
3152
|
function fmt(n) {
|
|
2015
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3153
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
2016
3154
|
return n.toLocaleString("en-US");
|
|
2017
3155
|
}
|
|
2018
3156
|
function printTokenFooter(filesScanned, mutationsMade) {
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
3157
|
+
const cached2 = getCachedValidation();
|
|
3158
|
+
if (!cached2) {
|
|
3159
|
+
process.stderr.write(chalk3.gray(`
|
|
3160
|
+
${filesScanned} files, ${mutationsMade} security tokens
|
|
3161
|
+
|
|
3162
|
+
`));
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const plan = cached2.plan;
|
|
3166
|
+
if (plan === "ENTERPRISE") {
|
|
2023
3167
|
process.stderr.write(
|
|
2024
|
-
|
|
2025
|
-
${filesScanned} files, ${mutationsMade}
|
|
3168
|
+
chalk3.gray(`
|
|
3169
|
+
${filesScanned} files, ${mutationsMade} security tokens \xB7 Plan: Enterprise (unlimited)
|
|
2026
3170
|
|
|
2027
3171
|
`)
|
|
2028
3172
|
);
|
|
2029
3173
|
return;
|
|
2030
3174
|
}
|
|
2031
|
-
const used =
|
|
2032
|
-
const cap =
|
|
2033
|
-
const
|
|
2034
|
-
const
|
|
3175
|
+
const used = cached2.mutationsUsed;
|
|
3176
|
+
const cap = cached2.monthlyMutationLimit;
|
|
3177
|
+
const unlimited = cap === -1 || !Number.isFinite(cap);
|
|
3178
|
+
const pct = !unlimited && cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
|
|
3179
|
+
const usageStr = unlimited ? chalk3.gray(`${fmt(used)} / unlimited`) : pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
|
|
2035
3180
|
process.stderr.write(
|
|
2036
3181
|
`
|
|
2037
|
-
${
|
|
3182
|
+
${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " security tokens")} \xB7 Plan: ${chalk3.bold(plan)} \xB7 Security tokens this month: ${usageStr}${!unlimited ? ` (${pct}%)` : ""}
|
|
2038
3183
|
`
|
|
2039
3184
|
);
|
|
2040
|
-
if (
|
|
3185
|
+
if (plan === "FREE") {
|
|
2041
3186
|
if (pct >= 80) {
|
|
2042
3187
|
process.stderr.write(
|
|
2043
|
-
|
|
3188
|
+
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold(PUBLISH_PACKAGE_URL)}
|
|
2044
3189
|
|
|
2045
3190
|
`)
|
|
2046
3191
|
);
|
|
2047
3192
|
} else {
|
|
2048
3193
|
process.stderr.write(
|
|
2049
|
-
|
|
3194
|
+
chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
|
|
2050
3195
|
|
|
2051
3196
|
`)
|
|
2052
3197
|
);
|
|
@@ -2057,7 +3202,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
2057
3202
|
}
|
|
2058
3203
|
|
|
2059
3204
|
// src/banner.ts
|
|
2060
|
-
import
|
|
3205
|
+
import chalk4 from "chalk";
|
|
2061
3206
|
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
|
|
2062
3207
|
\u2588\u2588 \u2588\u2588
|
|
2063
3208
|
\u2588\u2588 \u2588\u2588 Stop your code from
|
|
@@ -2077,7 +3222,7 @@ function shouldPrint() {
|
|
|
2077
3222
|
}
|
|
2078
3223
|
function tint(line) {
|
|
2079
3224
|
try {
|
|
2080
|
-
return
|
|
3225
|
+
return chalk4.hex(ICE_BLUE)(line);
|
|
2081
3226
|
} catch {
|
|
2082
3227
|
return line;
|
|
2083
3228
|
}
|
|
@@ -2092,53 +3237,66 @@ function printBanner() {
|
|
|
2092
3237
|
}
|
|
2093
3238
|
|
|
2094
3239
|
// src/commands/status.ts
|
|
2095
|
-
import
|
|
2096
|
-
var PLAN_LABEL = {
|
|
2097
|
-
free: "Free",
|
|
2098
|
-
pro: "Pro",
|
|
2099
|
-
enterprise: "Enterprise"
|
|
2100
|
-
};
|
|
3240
|
+
import chalk5 from "chalk";
|
|
2101
3241
|
function fmt2(n) {
|
|
2102
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3242
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
2103
3243
|
return n.toLocaleString("en-US");
|
|
2104
3244
|
}
|
|
2105
|
-
function runStatus() {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
2120
|
-
lines.push("");
|
|
2121
|
-
if (tier === "free") {
|
|
2122
|
-
lines.push(` ${chalk3.gray("Upgrade:")} ${chalk3.bold("pretense upgrade")}`);
|
|
2123
|
-
lines.push(` ${chalk3.gray("Credits:")} ${chalk3.bold("pretense credits")}`);
|
|
3245
|
+
async function runStatus() {
|
|
3246
|
+
let remote = await fetchUsageSummary();
|
|
3247
|
+
if (!remote) {
|
|
3248
|
+
const v = await validateKey({ force: true });
|
|
3249
|
+
if (v?.valid) {
|
|
3250
|
+
remote = usageSummaryFromValidate(v);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (remote) {
|
|
3254
|
+
const plan = remote.plan;
|
|
3255
|
+
const tierBadge = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3256
|
+
const lines = [];
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.green("(synced)")}`);
|
|
2124
3259
|
lines.push("");
|
|
3260
|
+
lines.push(` Plan: ${chalk5.bold(plan)}`);
|
|
3261
|
+
lines.push(` Security tokens:${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
|
|
3262
|
+
lines.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
|
|
3263
|
+
lines.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
|
|
3264
|
+
lines.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
|
|
3265
|
+
lines.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
|
|
3266
|
+
lines.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
3267
|
+
if (remote.keyExpiresAt) {
|
|
3268
|
+
lines.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
|
|
3269
|
+
}
|
|
3270
|
+
lines.push("");
|
|
3271
|
+
if (plan === "FREE") {
|
|
3272
|
+
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3273
|
+
lines.push("");
|
|
3274
|
+
}
|
|
3275
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3276
|
+
return;
|
|
2125
3277
|
}
|
|
2126
|
-
process.
|
|
3278
|
+
process.stderr.write(
|
|
3279
|
+
chalk5.yellow(`
|
|
3280
|
+
[pretense] Could not reach server \u2014 usage data unavailable.
|
|
3281
|
+
`) + chalk5.gray(` Check your connection or run \`pretense login\` if your key has changed.
|
|
3282
|
+
|
|
3283
|
+
`)
|
|
3284
|
+
);
|
|
2127
3285
|
}
|
|
2128
3286
|
|
|
2129
3287
|
// src/commands/upgrade.ts
|
|
2130
|
-
import
|
|
3288
|
+
import chalk6 from "chalk";
|
|
2131
3289
|
var PRICING_URL = "https://pretense.ai/pricing";
|
|
2132
3290
|
function runUpgrade() {
|
|
2133
3291
|
const tier = detectTier();
|
|
2134
3292
|
const lines = [];
|
|
2135
3293
|
lines.push("");
|
|
2136
|
-
lines.push(
|
|
3294
|
+
lines.push(chalk6.cyan.bold(" Pretense Plans"));
|
|
2137
3295
|
lines.push("");
|
|
2138
|
-
lines.push(
|
|
2139
|
-
lines.push(
|
|
2140
|
-
lines.push(
|
|
2141
|
-
lines.push(` \u2502 Price/seat \u2502 ${
|
|
3296
|
+
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"));
|
|
3297
|
+
lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
|
|
3298
|
+
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"));
|
|
3299
|
+
lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
|
|
2142
3300
|
lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
|
|
2143
3301
|
lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
|
|
2144
3302
|
lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
|
|
@@ -2146,159 +3304,215 @@ function runUpgrade() {
|
|
|
2146
3304
|
lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2147
3305
|
lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2148
3306
|
lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
|
|
2149
|
-
lines.push(
|
|
3307
|
+
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
3308
|
lines.push("");
|
|
2151
|
-
lines.push(` ${
|
|
2152
|
-
lines.push(` ${
|
|
3309
|
+
lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
|
|
3310
|
+
lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
|
|
2153
3311
|
lines.push("");
|
|
2154
|
-
lines.push(
|
|
2155
|
-
lines.push(
|
|
3312
|
+
lines.push(chalk6.gray(" After purchase, set:"));
|
|
3313
|
+
lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
|
2156
3314
|
lines.push("");
|
|
2157
3315
|
process.stdout.write(lines.join("\n") + "\n");
|
|
2158
3316
|
}
|
|
2159
3317
|
|
|
2160
3318
|
// src/commands/credits.ts
|
|
2161
|
-
import
|
|
2162
|
-
var PLAN_LABEL2 = {
|
|
2163
|
-
free: "Free",
|
|
2164
|
-
pro: "Pro",
|
|
2165
|
-
enterprise: "Enterprise"
|
|
2166
|
-
};
|
|
3319
|
+
import chalk7 from "chalk";
|
|
2167
3320
|
function fmt3(n) {
|
|
2168
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3321
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
2169
3322
|
return n.toLocaleString("en-US");
|
|
2170
3323
|
}
|
|
2171
3324
|
function progressBar(used, cap, width = 24) {
|
|
2172
|
-
if (!Number.isFinite(cap) || cap <= 0) return
|
|
3325
|
+
if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
|
|
2173
3326
|
const pct = Math.min(1, used / cap);
|
|
2174
3327
|
const filled = Math.round(pct * width);
|
|
2175
3328
|
const bar = "=".repeat(filled) + "-".repeat(width - filled);
|
|
2176
|
-
const colored = pct >= 0.8 ?
|
|
3329
|
+
const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
|
|
2177
3330
|
return "[" + colored + "]";
|
|
2178
3331
|
}
|
|
2179
|
-
function runCredits() {
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
lines.push(` Used: ${pct}%`);
|
|
2196
|
-
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
2197
|
-
lines.push("");
|
|
2198
|
-
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
2199
|
-
lines.push(chalk5.yellow(" \u26A0 Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3332
|
+
async function runCredits() {
|
|
3333
|
+
let remote = await fetchUsageSummary();
|
|
3334
|
+
if (!remote) {
|
|
3335
|
+
const v = await validateKey({ force: true });
|
|
3336
|
+
if (v?.valid) {
|
|
3337
|
+
remote = usageSummaryFromValidate(v);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
if (remote) {
|
|
3341
|
+
const used = remote.mutationsUsed;
|
|
3342
|
+
const cap = remote.monthlyMutationLimit;
|
|
3343
|
+
const remaining = remote.mutationsRemaining;
|
|
3344
|
+
const pct = cap > 0 && cap !== -1 ? Math.round(used / cap * 100) : 0;
|
|
3345
|
+
const lines = [];
|
|
3346
|
+
lines.push("");
|
|
3347
|
+
lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
|
|
2200
3348
|
lines.push("");
|
|
2201
|
-
|
|
2202
|
-
lines.push(
|
|
3349
|
+
lines.push(` Plan: ${chalk7.bold(remote.plan)}`);
|
|
3350
|
+
lines.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
2203
3351
|
lines.push("");
|
|
3352
|
+
lines.push(` Security tokens:${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
3353
|
+
lines.push(` Used: ${pct}%`);
|
|
3354
|
+
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
3355
|
+
lines.push("");
|
|
3356
|
+
if (remote.plan === "FREE" && cap !== -1 && used >= cap * 0.8) {
|
|
3357
|
+
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3358
|
+
lines.push("");
|
|
3359
|
+
} else if (remote.plan === "FREE") {
|
|
3360
|
+
lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
3361
|
+
lines.push("");
|
|
3362
|
+
}
|
|
3363
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3364
|
+
return;
|
|
2204
3365
|
}
|
|
2205
|
-
process.
|
|
2206
|
-
|
|
3366
|
+
process.stderr.write(
|
|
3367
|
+
chalk7.yellow(`
|
|
3368
|
+
[pretense] Could not reach server \u2014 usage data unavailable.
|
|
3369
|
+
`) + chalk7.gray(` Check your connection or run \`pretense login\` if your key has changed.
|
|
2207
3370
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
import { join as join4 } from "path";
|
|
2211
|
-
import { homedir as homedir2 } from "os";
|
|
2212
|
-
import chalk6 from "chalk";
|
|
2213
|
-
var CONFIG_DIR_NAME2 = ".pretense";
|
|
2214
|
-
var CONFIG_FILE2 = "config.json";
|
|
2215
|
-
function getUserConfigDir() {
|
|
2216
|
-
return join4(homedir2(), CONFIG_DIR_NAME2);
|
|
2217
|
-
}
|
|
2218
|
-
function isValidKeyShape(key) {
|
|
2219
|
-
const trimmed = key.trim();
|
|
2220
|
-
if (trimmed.length < 24) return false;
|
|
2221
|
-
if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
|
|
2222
|
-
return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_");
|
|
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)}`;
|
|
3371
|
+
`)
|
|
3372
|
+
);
|
|
2247
3373
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
3374
|
+
|
|
3375
|
+
// src/commands/logout.ts
|
|
3376
|
+
import readline2 from "readline/promises";
|
|
3377
|
+
import chalk8 from "chalk";
|
|
3378
|
+
async function runLogout(opts = {}) {
|
|
3379
|
+
if (!loadApiKey()) {
|
|
3380
|
+
console.log(chalk8.gray("No saved Pretense dashboard API key in ~/.pretense/config.json."));
|
|
3381
|
+
console.log(chalk8.gray("If you use PRETENSE_API_KEY in your shell, run unset PRETENSE_API_KEY."));
|
|
3382
|
+
return 0;
|
|
3383
|
+
}
|
|
3384
|
+
if (!opts.assumeYes) {
|
|
3385
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3386
|
+
console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
|
|
3387
|
+
console.error(chalk8.gray("Run `pretense logout --yes` to skip the prompt."));
|
|
3388
|
+
return 1;
|
|
3389
|
+
}
|
|
3390
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
2255
3391
|
try {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
3392
|
+
const raw = await rl.question(
|
|
3393
|
+
chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
|
|
3394
|
+
);
|
|
3395
|
+
const answer = raw.trim().toLowerCase();
|
|
3396
|
+
if (answer !== "y" && answer !== "yes") {
|
|
3397
|
+
console.log(chalk8.yellow("Logout cancelled."));
|
|
3398
|
+
return 0;
|
|
3399
|
+
}
|
|
3400
|
+
} finally {
|
|
3401
|
+
rl.close();
|
|
2259
3402
|
}
|
|
2260
3403
|
}
|
|
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;
|
|
3404
|
+
const hadEnvDashboardKey = !!process.env["PRETENSE_API_KEY"]?.trim();
|
|
3405
|
+
const { removed, path } = removeSavedDashboardCredentials();
|
|
3406
|
+
clearValidationCache();
|
|
3407
|
+
delete process.env["PRETENSE_API_KEY"];
|
|
3408
|
+
if (removed) {
|
|
3409
|
+
console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
|
|
3410
|
+
console.log(chalk8.gray("Other terminals: run unset PRETENSE_API_KEY if that variable is still exported (e.g. in ~/.zshrc)."));
|
|
3411
|
+
if (hadEnvDashboardKey) {
|
|
3412
|
+
console.log(
|
|
3413
|
+
chalk8.yellow(
|
|
3414
|
+
"This shell had PRETENSE_API_KEY set; it was cleared for this process only. Restart any running pretense proxy (Ctrl+C, then pretense start)."
|
|
3415
|
+
)
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
2294
3418
|
}
|
|
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
3419
|
return 0;
|
|
2300
3420
|
}
|
|
2301
3421
|
|
|
3422
|
+
// src/commands/install.ts
|
|
3423
|
+
import { Command } from "commander";
|
|
3424
|
+
import chalk9 from "chalk";
|
|
3425
|
+
import {
|
|
3426
|
+
existsSync as existsSync7,
|
|
3427
|
+
writeFileSync as writeFileSync5,
|
|
3428
|
+
chmodSync as chmodSync2,
|
|
3429
|
+
readFileSync as readFileSync7,
|
|
3430
|
+
mkdirSync as mkdirSync6
|
|
3431
|
+
} from "fs";
|
|
3432
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
3433
|
+
var HOOK_SCRIPTS = {
|
|
3434
|
+
"pre-commit": `#!/bin/sh
|
|
3435
|
+
# Pretense AI Firewall \u2014 pre-commit hook
|
|
3436
|
+
# Installed by: pretense install --mode pre-commit
|
|
3437
|
+
|
|
3438
|
+
pretense scan pre-commit
|
|
3439
|
+
exit $?
|
|
3440
|
+
`,
|
|
3441
|
+
"pre-push": `#!/bin/sh
|
|
3442
|
+
# Pretense AI Firewall \u2014 pre-push hook
|
|
3443
|
+
# Installed by: pretense install --mode pre-push
|
|
3444
|
+
|
|
3445
|
+
pretense scan pre-push
|
|
3446
|
+
exit $?
|
|
3447
|
+
`
|
|
3448
|
+
};
|
|
3449
|
+
var VALID_MODES = ["pre-commit", "pre-push"];
|
|
3450
|
+
function findGitDir(cwd) {
|
|
3451
|
+
const gitDir = join5(cwd, ".git");
|
|
3452
|
+
if (existsSync7(gitDir)) return gitDir;
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
function installHook(hooksDir, mode, force) {
|
|
3456
|
+
const hookPath = join5(hooksDir, mode);
|
|
3457
|
+
const script = HOOK_SCRIPTS[mode];
|
|
3458
|
+
if (existsSync7(hookPath) && !force) {
|
|
3459
|
+
const existing = readFileSync7(hookPath, "utf-8");
|
|
3460
|
+
if (existing.includes("Pretense AI Firewall")) {
|
|
3461
|
+
return { installed: false, path: hookPath, skipped: true };
|
|
3462
|
+
}
|
|
3463
|
+
return { installed: false, path: hookPath };
|
|
3464
|
+
}
|
|
3465
|
+
writeFileSync5(hookPath, script, "utf-8");
|
|
3466
|
+
chmodSync2(hookPath, 493);
|
|
3467
|
+
return { installed: true, path: hookPath };
|
|
3468
|
+
}
|
|
3469
|
+
function installCommand() {
|
|
3470
|
+
return new Command("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option("-m, --mode <mode>", "Hook mode: pre-commit, pre-push (omit to install both)").option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
|
|
3471
|
+
const dir = resolve2(opts.dir);
|
|
3472
|
+
const force = opts.force;
|
|
3473
|
+
const modeArg = opts.mode;
|
|
3474
|
+
if (modeArg && !VALID_MODES.includes(modeArg)) {
|
|
3475
|
+
console.error(
|
|
3476
|
+
chalk9.red(`
|
|
3477
|
+
Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
|
|
3478
|
+
`)
|
|
3479
|
+
);
|
|
3480
|
+
process.exit(1);
|
|
3481
|
+
}
|
|
3482
|
+
const modes = modeArg ? [modeArg] : [...VALID_MODES];
|
|
3483
|
+
const gitDir = findGitDir(dir);
|
|
3484
|
+
if (!gitDir) {
|
|
3485
|
+
console.error(
|
|
3486
|
+
chalk9.red(`
|
|
3487
|
+
Error: No .git directory found in ${dir}
|
|
3488
|
+
Run this command from a git repository root.
|
|
3489
|
+
`)
|
|
3490
|
+
);
|
|
3491
|
+
process.exit(1);
|
|
3492
|
+
}
|
|
3493
|
+
const hooksDir = join5(gitDir, "hooks");
|
|
3494
|
+
mkdirSync6(hooksDir, { recursive: true });
|
|
3495
|
+
console.log(chalk9.cyan("\n Pretense hook installer\n"));
|
|
3496
|
+
let hasError = false;
|
|
3497
|
+
for (const mode of modes) {
|
|
3498
|
+
const result = installHook(hooksDir, mode, force);
|
|
3499
|
+
if (result.installed) {
|
|
3500
|
+
console.log(chalk9.green(` Installed ${mode} hook at ${result.path}`));
|
|
3501
|
+
} else if (result.skipped) {
|
|
3502
|
+
console.log(chalk9.dim(` Pretense ${mode} hook already installed at ${result.path}`));
|
|
3503
|
+
} else {
|
|
3504
|
+
console.error(chalk9.yellow(` Hook already exists at ${result.path}. Use --force to overwrite.`));
|
|
3505
|
+
hasError = true;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
if (hasError) {
|
|
3509
|
+
console.log("");
|
|
3510
|
+
process.exit(1);
|
|
3511
|
+
}
|
|
3512
|
+
console.log(chalk9.bold.green("\n Git hooks installed successfully.\n"));
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
|
|
2302
3516
|
// src/index.ts
|
|
2303
3517
|
{
|
|
2304
3518
|
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
@@ -2312,7 +3526,7 @@ function runLogin(opts = {}) {
|
|
|
2312
3526
|
process.exit(1);
|
|
2313
3527
|
}
|
|
2314
3528
|
}
|
|
2315
|
-
var VERSION =
|
|
3529
|
+
var VERSION = getCliSemver();
|
|
2316
3530
|
var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
|
|
2317
3531
|
var PROGRESS_INTERVAL_MS = 500;
|
|
2318
3532
|
var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
|
|
@@ -2435,18 +3649,35 @@ function validateLanguage(lang) {
|
|
|
2435
3649
|
return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
|
|
2436
3650
|
}
|
|
2437
3651
|
function collectFiles(target) {
|
|
2438
|
-
const abs =
|
|
2439
|
-
if (!
|
|
3652
|
+
const abs = resolve3(target);
|
|
3653
|
+
if (!existsSync8(abs)) {
|
|
2440
3654
|
return [];
|
|
2441
3655
|
}
|
|
2442
3656
|
const stat = statSync(abs);
|
|
2443
3657
|
if (stat.isFile()) return [abs];
|
|
2444
3658
|
const files = [];
|
|
2445
3659
|
function walk(dir) {
|
|
2446
|
-
|
|
2447
|
-
|
|
3660
|
+
let entries;
|
|
3661
|
+
try {
|
|
3662
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3663
|
+
} catch {
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
3667
|
+
"node_modules",
|
|
3668
|
+
".git",
|
|
3669
|
+
"dist",
|
|
3670
|
+
"build",
|
|
3671
|
+
".pretest",
|
|
3672
|
+
".pretense",
|
|
3673
|
+
".next",
|
|
3674
|
+
"__pycache__",
|
|
3675
|
+
".Trash"
|
|
3676
|
+
]);
|
|
3677
|
+
for (const entry of entries) {
|
|
3678
|
+
const fullPath = join6(dir, entry.name);
|
|
2448
3679
|
if (entry.isDirectory()) {
|
|
2449
|
-
if (
|
|
3680
|
+
if (skipDirs.has(entry.name)) continue;
|
|
2450
3681
|
walk(fullPath);
|
|
2451
3682
|
} else if (entry.isFile() && isScannableFile(entry.name)) {
|
|
2452
3683
|
files.push(fullPath);
|
|
@@ -2456,13 +3687,13 @@ function collectFiles(target) {
|
|
|
2456
3687
|
walk(abs);
|
|
2457
3688
|
return files;
|
|
2458
3689
|
}
|
|
2459
|
-
var program = new
|
|
3690
|
+
var program = new Command2();
|
|
2460
3691
|
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
|
|
2461
3692
|
printBanner();
|
|
2462
3693
|
});
|
|
2463
3694
|
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
2464
|
-
const dir =
|
|
2465
|
-
console.log(
|
|
3695
|
+
const dir = resolve3(opts.dir);
|
|
3696
|
+
console.log(chalk10.cyan("Initializing Pretense in"), chalk10.bold(dir));
|
|
2466
3697
|
const configDir = initConfig(dir);
|
|
2467
3698
|
const files = collectFiles(dir);
|
|
2468
3699
|
const langCounts = {};
|
|
@@ -2470,23 +3701,25 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
2470
3701
|
const lang = langFromExt(f);
|
|
2471
3702
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
2472
3703
|
}
|
|
2473
|
-
console.log(
|
|
2474
|
-
console.log(
|
|
3704
|
+
console.log(chalk10.green("\n .pretense/ created at"), chalk10.bold(configDir));
|
|
3705
|
+
console.log(chalk10.gray(`
|
|
2475
3706
|
Scanned ${files.length} source files:`));
|
|
2476
3707
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
2477
|
-
console.log(
|
|
3708
|
+
console.log(chalk10.gray(` ${lang}: ${count} files`));
|
|
2478
3709
|
}
|
|
2479
|
-
console.log(
|
|
3710
|
+
console.log(chalk10.cyan("\n Next: run"), chalk10.bold("pretense start"), chalk10.cyan("to launch the proxy"));
|
|
2480
3711
|
console.log();
|
|
2481
3712
|
});
|
|
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) => {
|
|
3713
|
+
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
3714
|
const config = loadConfig();
|
|
2484
3715
|
const port = validatePort(opts.port);
|
|
2485
3716
|
if (port === null) {
|
|
2486
|
-
console.error(
|
|
2487
|
-
console.error(
|
|
3717
|
+
console.error(chalk10.red("Error: Invalid port number:"), chalk10.bold(opts.port));
|
|
3718
|
+
console.error(chalk10.gray("Port must be an integer between 1 and 65535."));
|
|
2488
3719
|
process.exit(1);
|
|
2489
3720
|
}
|
|
3721
|
+
await ensureDashboardKeyForStart();
|
|
3722
|
+
logDashboardCredentialHint();
|
|
2490
3723
|
startProxy({ config, port, verbose: opts.verbose });
|
|
2491
3724
|
});
|
|
2492
3725
|
async function scanOneFile(file, opts) {
|
|
@@ -2595,30 +3828,30 @@ function parseSizeOption(value) {
|
|
|
2595
3828
|
return Math.floor(n * mult);
|
|
2596
3829
|
}
|
|
2597
3830
|
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
|
-
const absTarget =
|
|
2599
|
-
if (!
|
|
2600
|
-
console.error(
|
|
3831
|
+
const absTarget = resolve3(target);
|
|
3832
|
+
if (!existsSync8(absTarget)) {
|
|
3833
|
+
console.error(chalk10.red("Error: Path not found:"), chalk10.bold(absTarget));
|
|
2601
3834
|
process.exit(1);
|
|
2602
3835
|
}
|
|
2603
3836
|
const maxFileSize = parseSizeOption(opts.maxFileSize);
|
|
2604
3837
|
if (maxFileSize === null) {
|
|
2605
|
-
console.error(
|
|
2606
|
-
console.error(
|
|
3838
|
+
console.error(chalk10.red("Error: Invalid --max-file-size value:"), chalk10.bold(opts.maxFileSize));
|
|
3839
|
+
console.error(chalk10.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
|
|
2607
3840
|
process.exit(1);
|
|
2608
3841
|
}
|
|
2609
3842
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
|
|
2610
3843
|
if (!opts.quiet && !opts.json) {
|
|
2611
|
-
process.stderr.write(
|
|
3844
|
+
process.stderr.write(chalk10.gray(`Discovering files in ${absTarget}...
|
|
2612
3845
|
`));
|
|
2613
3846
|
}
|
|
2614
3847
|
const files = collectFiles(target);
|
|
2615
3848
|
if (files.length === 0) {
|
|
2616
|
-
console.error(
|
|
2617
|
-
console.error(
|
|
3849
|
+
console.error(chalk10.red("Error: No source files found at"), chalk10.bold(absTarget));
|
|
3850
|
+
console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
2618
3851
|
process.exit(1);
|
|
2619
3852
|
}
|
|
2620
3853
|
if (!opts.quiet && !opts.json) {
|
|
2621
|
-
process.stderr.write(
|
|
3854
|
+
process.stderr.write(chalk10.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
|
|
2622
3855
|
`));
|
|
2623
3856
|
}
|
|
2624
3857
|
const startedAt = Date.now();
|
|
@@ -2644,34 +3877,34 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
2644
3877
|
interrupted
|
|
2645
3878
|
}, null, 2));
|
|
2646
3879
|
} else {
|
|
2647
|
-
console.log(
|
|
3880
|
+
console.log(chalk10.cyan("\nPretense Scan Results\n"));
|
|
2648
3881
|
for (const r of allResults) {
|
|
2649
3882
|
if (r.skipped) {
|
|
2650
3883
|
if (r.skipped === "too-large") {
|
|
2651
3884
|
const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
|
|
2652
|
-
console.log(
|
|
3885
|
+
console.log(chalk10.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
|
|
2653
3886
|
}
|
|
2654
3887
|
continue;
|
|
2655
3888
|
}
|
|
2656
|
-
const secretBadge = r.secrets > 0 ?
|
|
3889
|
+
const secretBadge = r.secrets > 0 ? chalk10.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
2657
3890
|
const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
|
|
2658
3891
|
if (!showRow) continue;
|
|
2659
3892
|
console.log(
|
|
2660
|
-
|
|
3893
|
+
chalk10.gray(" ") + chalk10.white(r.file) + chalk10.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
2661
3894
|
);
|
|
2662
3895
|
if (r.secrets > 0) {
|
|
2663
3896
|
for (const t of r.secretTypes) {
|
|
2664
|
-
console.log(
|
|
3897
|
+
console.log(chalk10.red(` ! ${t}`));
|
|
2665
3898
|
}
|
|
2666
3899
|
}
|
|
2667
3900
|
}
|
|
2668
|
-
const skipNote = skippedLarge + skippedBinary > 0 ?
|
|
3901
|
+
const skipNote = skippedLarge + skippedBinary > 0 ? chalk10.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
|
|
2669
3902
|
console.log(
|
|
2670
|
-
|
|
2671
|
-
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ?
|
|
3903
|
+
chalk10.gray(`
|
|
3904
|
+
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk10.red(`${totalSecrets} secrets found`) : chalk10.green("0 secrets")) + skipNote
|
|
2672
3905
|
);
|
|
2673
3906
|
if (interrupted) {
|
|
2674
|
-
console.log(
|
|
3907
|
+
console.log(chalk10.yellow("\n Interrupted \u2014 partial results shown above."));
|
|
2675
3908
|
}
|
|
2676
3909
|
console.log();
|
|
2677
3910
|
}
|
|
@@ -2685,68 +3918,81 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
2685
3918
|
process.exit(2);
|
|
2686
3919
|
}
|
|
2687
3920
|
});
|
|
2688
|
-
program.command("mutate <file>").description("Mutate
|
|
2689
|
-
const absPath =
|
|
2690
|
-
if (!
|
|
2691
|
-
console.error(
|
|
3921
|
+
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) => {
|
|
3922
|
+
const absPath = resolve3(file);
|
|
3923
|
+
if (!existsSync8(absPath)) {
|
|
3924
|
+
console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
|
|
2692
3925
|
process.exit(1);
|
|
2693
3926
|
}
|
|
2694
3927
|
if (opts.language) {
|
|
2695
3928
|
const validLang = validateLanguage(opts.language);
|
|
2696
3929
|
if (!validLang) {
|
|
2697
|
-
console.error(
|
|
2698
|
-
console.error(
|
|
3930
|
+
console.error(chalk10.red("Error: Unsupported language:"), chalk10.bold(opts.language));
|
|
3931
|
+
console.error(chalk10.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
|
|
2699
3932
|
process.exit(1);
|
|
2700
3933
|
}
|
|
2701
3934
|
}
|
|
2702
3935
|
if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
|
|
2703
|
-
console.error(
|
|
2704
|
-
console.error(
|
|
3936
|
+
console.error(chalk10.red("Error: Not a supported source file:"), chalk10.bold(absPath));
|
|
3937
|
+
console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
2705
3938
|
process.exit(1);
|
|
2706
3939
|
}
|
|
2707
|
-
const code =
|
|
3940
|
+
const code = readFileSync8(absPath, "utf-8");
|
|
2708
3941
|
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
2709
|
-
const
|
|
3942
|
+
const secretScan = scan2(code);
|
|
3943
|
+
const effectiveSeed = effectiveSeedForMutation(opts.seed);
|
|
3944
|
+
const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
|
|
3945
|
+
const secretsRedacted = secretsMap.size;
|
|
3946
|
+
const result = mutate(redacted, lang, opts.seed);
|
|
2710
3947
|
if (opts.save) {
|
|
2711
3948
|
const configDir = getConfigDir();
|
|
2712
|
-
const store = new MutationStore(
|
|
3949
|
+
const store = new MutationStore(join6(configDir, "mutation-map.json"));
|
|
2713
3950
|
store.load();
|
|
2714
3951
|
store.save({
|
|
2715
3952
|
id: absPath,
|
|
2716
3953
|
originalHash: absPath,
|
|
2717
3954
|
mapEntries: MutationStore.fromMap(result.map),
|
|
3955
|
+
secretsMapEntries: [...secretsMap.entries()],
|
|
2718
3956
|
timestamp: Date.now(),
|
|
2719
3957
|
language: lang
|
|
2720
3958
|
});
|
|
2721
3959
|
store.persist();
|
|
2722
|
-
process.stderr.write(
|
|
3960
|
+
process.stderr.write(chalk10.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
2723
3961
|
`));
|
|
2724
3962
|
}
|
|
2725
3963
|
if (opts.json) {
|
|
2726
3964
|
console.log(JSON.stringify({
|
|
2727
3965
|
mutatedCode: result.mutatedCode,
|
|
2728
3966
|
map: Object.fromEntries(result.map),
|
|
2729
|
-
stats: result.stats
|
|
3967
|
+
stats: result.stats,
|
|
3968
|
+
secretsRedacted,
|
|
3969
|
+
secretsMap: Object.fromEntries(secretsMap)
|
|
2730
3970
|
}, null, 2));
|
|
2731
3971
|
} else {
|
|
2732
3972
|
process.stdout.write(result.mutatedCode);
|
|
2733
|
-
|
|
2734
|
-
|
|
3973
|
+
if (secretsRedacted > 0) {
|
|
3974
|
+
process.stderr.write(
|
|
3975
|
+
chalk10.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before code protection ---
|
|
3976
|
+
`)
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
process.stderr.write(chalk10.gray(`
|
|
3980
|
+
--- ${result.stats.tokensMutated} identifiers protected in ${result.stats.durationMs}ms ---
|
|
2735
3981
|
`));
|
|
2736
3982
|
printTokenFooter(1, result.stats.tokensMutated);
|
|
2737
3983
|
}
|
|
2738
3984
|
});
|
|
2739
3985
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
2740
|
-
const absPath =
|
|
2741
|
-
if (!
|
|
2742
|
-
console.error(
|
|
3986
|
+
const absPath = resolve3(file);
|
|
3987
|
+
if (!existsSync8(absPath)) {
|
|
3988
|
+
console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
|
|
2743
3989
|
process.exit(1);
|
|
2744
3990
|
}
|
|
2745
|
-
const mutatedCode =
|
|
2746
|
-
const mapPath = opts.map ?
|
|
2747
|
-
if (!
|
|
2748
|
-
console.error(
|
|
2749
|
-
console.error(
|
|
3991
|
+
const mutatedCode = readFileSync8(absPath, "utf-8");
|
|
3992
|
+
const mapPath = opts.map ? resolve3(opts.map) : join6(getConfigDir(), "mutation-map.json");
|
|
3993
|
+
if (!existsSync8(mapPath)) {
|
|
3994
|
+
console.error(chalk10.red("Error: Mutation map not found:"), chalk10.bold(mapPath));
|
|
3995
|
+
console.error(chalk10.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
|
|
2750
3996
|
process.exit(1);
|
|
2751
3997
|
}
|
|
2752
3998
|
let store;
|
|
@@ -2754,21 +4000,24 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
|
|
|
2754
4000
|
store = new MutationStore(mapPath);
|
|
2755
4001
|
store.load();
|
|
2756
4002
|
} catch {
|
|
2757
|
-
console.error(
|
|
2758
|
-
console.error(
|
|
4003
|
+
console.error(chalk10.red("Error: Failed to load mutation map:"), chalk10.bold(mapPath));
|
|
4004
|
+
console.error(chalk10.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
|
|
2759
4005
|
process.exit(1);
|
|
2760
4006
|
}
|
|
2761
4007
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
2762
4008
|
if (!entry) {
|
|
2763
|
-
console.error(
|
|
2764
|
-
console.error(
|
|
4009
|
+
console.error(chalk10.red("Error: No mutation map entries found."));
|
|
4010
|
+
console.error(chalk10.gray("Run 'pretense mutate --save <file>' first to generate a map."));
|
|
2765
4011
|
process.exit(1);
|
|
2766
4012
|
}
|
|
2767
4013
|
const map = MutationStore.toMap(entry);
|
|
2768
|
-
const
|
|
4014
|
+
const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
|
|
4015
|
+
const restored = reverse(mutatedCode, map, secretsMap);
|
|
2769
4016
|
process.stdout.write(restored);
|
|
2770
|
-
|
|
2771
|
-
|
|
4017
|
+
const secretsCount = secretsMap?.size ?? 0;
|
|
4018
|
+
const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
|
|
4019
|
+
process.stderr.write(chalk10.gray(`
|
|
4020
|
+
--- Reversed ${map.size} security tokens${secretsSuffix} ---
|
|
2772
4021
|
`));
|
|
2773
4022
|
});
|
|
2774
4023
|
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 +4030,35 @@ program.command("audit").description("View the audit log").option("--json", "Out
|
|
|
2781
4030
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
2782
4031
|
console.log(`@pretense/cli v${VERSION}`);
|
|
2783
4032
|
});
|
|
2784
|
-
program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
|
|
2785
|
-
runStatus();
|
|
4033
|
+
program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
|
|
4034
|
+
await runStatus();
|
|
2786
4035
|
});
|
|
2787
|
-
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
|
|
2788
|
-
runStatus();
|
|
4036
|
+
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
|
|
4037
|
+
await runStatus();
|
|
2789
4038
|
});
|
|
2790
|
-
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining
|
|
2791
|
-
runCredits();
|
|
4039
|
+
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining security token budget").action(async () => {
|
|
4040
|
+
await runCredits();
|
|
2792
4041
|
});
|
|
2793
4042
|
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
2794
4043
|
runUpgrade();
|
|
2795
4044
|
});
|
|
2796
|
-
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
|
|
2797
|
-
runCredits();
|
|
4045
|
+
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
|
|
4046
|
+
await runCredits();
|
|
2798
4047
|
});
|
|
2799
|
-
program.command("login").description("Save a
|
|
4048
|
+
program.command("login").description("Save a dashboard 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
4049
|
const code = runLogin({ key: opts.key });
|
|
2801
4050
|
if (code !== 0) process.exit(code);
|
|
2802
4051
|
});
|
|
4052
|
+
program.command("logout").description("Remove the saved dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
|
|
4053
|
+
const code = await runLogout({ assumeYes: opts.yes === true });
|
|
4054
|
+
if (code !== 0) process.exit(code);
|
|
4055
|
+
});
|
|
4056
|
+
program.addCommand(installCommand());
|
|
2803
4057
|
function handleFatalError(err) {
|
|
2804
4058
|
if (err instanceof Error && err.message) {
|
|
2805
|
-
console.error(
|
|
4059
|
+
console.error(chalk10.red("Error:"), err.message);
|
|
2806
4060
|
} else {
|
|
2807
|
-
console.error(
|
|
4061
|
+
console.error(chalk10.red("An unexpected error occurred."));
|
|
2808
4062
|
}
|
|
2809
4063
|
process.exit(1);
|
|
2810
4064
|
}
|