claude-code-infinite 1.0.34 → 1.0.36
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 +56 -0
- package/dist/body-shape.d.ts +49 -0
- package/dist/body-shape.d.ts.map +1 -0
- package/dist/body-shape.js +232 -0
- package/dist/body-shape.js.map +1 -0
- package/dist/cli-main.js +19 -2
- package/dist/cli-main.js.map +1 -1
- package/dist/cli.js +23 -10
- package/dist/cli.js.map +1 -1
- package/dist/count-tokens.d.ts +15 -2
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +13 -4
- package/dist/count-tokens.js.map +1 -1
- package/dist/financial-redaction.d.ts +12 -0
- package/dist/financial-redaction.d.ts.map +1 -0
- package/dist/financial-redaction.js +509 -0
- package/dist/financial-redaction.js.map +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +4 -0
- package/dist/hooks.js.map +1 -1
- package/dist/image-tokens.d.ts +6 -1
- package/dist/image-tokens.d.ts.map +1 -1
- package/dist/image-tokens.js +75 -14
- package/dist/image-tokens.js.map +1 -1
- package/dist/lane-activity.d.ts +42 -0
- package/dist/lane-activity.d.ts.map +1 -0
- package/dist/lane-activity.js +79 -0
- package/dist/lane-activity.js.map +1 -0
- package/dist/memtree-newest.d.ts +11 -3
- package/dist/memtree-newest.d.ts.map +1 -1
- package/dist/memtree-newest.js +24 -4
- package/dist/memtree-newest.js.map +1 -1
- package/dist/memtree.d.ts +10 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +14 -0
- package/dist/memtree.js.map +1 -1
- package/dist/proxy.d.ts +21 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +153 -12
- package/dist/proxy.js.map +1 -1
- package/dist/refusal-reason.d.ts +70 -0
- package/dist/refusal-reason.d.ts.map +1 -0
- package/dist/refusal-reason.js +132 -0
- package/dist/refusal-reason.js.map +1 -0
- package/dist/reqlog.d.ts +25 -2
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +38 -2
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +72 -6
- package/dist/request-sizing.js.map +1 -1
- package/dist/route-fallback.d.ts +2 -0
- package/dist/route-fallback.d.ts.map +1 -1
- package/dist/route-fallback.js +8 -8
- package/dist/route-fallback.js.map +1 -1
- package/dist/secret-redaction.d.ts +25 -0
- package/dist/secret-redaction.d.ts.map +1 -0
- package/dist/secret-redaction.js +492 -0
- package/dist/secret-redaction.js.map +1 -0
- package/dist/slow-upload.d.ts +40 -0
- package/dist/slow-upload.d.ts.map +1 -0
- package/dist/slow-upload.js +69 -0
- package/dist/slow-upload.js.map +1 -0
- package/dist/status-link.d.ts +18 -0
- package/dist/status-link.d.ts.map +1 -0
- package/dist/status-link.js +103 -0
- package/dist/status-link.js.map +1 -0
- package/dist/statusline-auto.d.ts +15 -0
- package/dist/statusline-auto.d.ts.map +1 -0
- package/dist/statusline-auto.js +125 -0
- package/dist/statusline-auto.js.map +1 -0
- package/dist/statusline.d.ts +51 -0
- package/dist/statusline.d.ts.map +1 -0
- package/dist/statusline.js +380 -0
- package/dist/statusline.js.map +1 -0
- package/dist/utf8-bytes.d.ts +10 -0
- package/dist/utf8-bytes.d.ts.map +1 -0
- package/dist/utf8-bytes.js +107 -0
- package/dist/utf8-bytes.js.map +1 -0
- package/package.json +1 -1
package/dist/route-fallback.js
CHANGED
|
@@ -51,11 +51,11 @@ export class RouteFallbackFailures {
|
|
|
51
51
|
}
|
|
52
52
|
fail(lane, context = {}) {
|
|
53
53
|
if (context.awaitingIndex) {
|
|
54
|
-
const waiting = this.waitForIndex(lane, context.now ?? Date.now(), context.indexSecondsLeft);
|
|
54
|
+
const waiting = this.waitForIndex(lane, context.now ?? Date.now(), context.indexSecondsLeft, context.refusalMessage);
|
|
55
55
|
if (waiting)
|
|
56
56
|
return waiting;
|
|
57
57
|
}
|
|
58
|
-
return this.strike(lane);
|
|
58
|
+
return this.strike(lane, context.refusalMessage);
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Note MemTree saying the lane's index is still catching up (a held request's 413):
|
|
@@ -98,7 +98,7 @@ export class RouteFallbackFailures {
|
|
|
98
98
|
this.indexWaits.delete(lane);
|
|
99
99
|
}
|
|
100
100
|
/** The bounded three-strike episode: 503, 503, then 400 until delivery. */
|
|
101
|
-
strike(lane) {
|
|
101
|
+
strike(lane, message) {
|
|
102
102
|
const previous = this.failures.get(lane);
|
|
103
103
|
// Fail closed when full rather than evicting a lane and granting it
|
|
104
104
|
// another retry budget. A successful lane frees its slot.
|
|
@@ -107,7 +107,7 @@ export class RouteFallbackFailures {
|
|
|
107
107
|
if (previous !== undefined || this.failures.size < this.capacity) {
|
|
108
108
|
this.failures.set(lane, attempt);
|
|
109
109
|
}
|
|
110
|
-
return attempt <= 2 ? retryError(attempt, "1", RETRY_MESSAGE) : refusal(attempt);
|
|
110
|
+
return attempt <= 2 ? retryError(attempt, "1", RETRY_MESSAGE) : refusal(attempt, message);
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* A 503 while the lane's wait is within its deadline; a 400 once it passes, or at once
|
|
@@ -118,11 +118,11 @@ export class RouteFallbackFailures {
|
|
|
118
118
|
* or arriving while every wait slot is active, gets no wait (undefined) and falls back
|
|
119
119
|
* to the strikes.
|
|
120
120
|
*/
|
|
121
|
-
waitForIndex(lane, now, secondsLeft) {
|
|
121
|
+
waitForIndex(lane, now, secondsLeft, message) {
|
|
122
122
|
const state = this.track(lane, now, secondsLeft);
|
|
123
123
|
if (state === "untracked")
|
|
124
124
|
return undefined;
|
|
125
|
-
return state === "open" ? this.indexWaitError(lane, secondsLeft) : refusal(3);
|
|
125
|
+
return state === "open" ? this.indexWaitError(lane, secondsLeft) : refusal(3, message);
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
128
128
|
* Start or extend the lane's wait: "open" while the index may still land in time,
|
|
@@ -245,14 +245,14 @@ function retryError(attempt, retryAfter, message) {
|
|
|
245
245
|
body: { type: "error", error: { type: "api_error", message } },
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
-
function refusal(attempt) {
|
|
248
|
+
function refusal(attempt, message = FINAL_REFUSAL_MESSAGE) {
|
|
249
249
|
return {
|
|
250
250
|
status: 400,
|
|
251
251
|
attempt,
|
|
252
252
|
headers: { "x-should-retry": "false" },
|
|
253
253
|
body: {
|
|
254
254
|
type: "error",
|
|
255
|
-
error: { type: "invalid_request_error", message
|
|
255
|
+
error: { type: "invalid_request_error", message },
|
|
256
256
|
},
|
|
257
257
|
};
|
|
258
258
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-fallback.js","sourceRoot":"","sources":["../src/route-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEhG;;oEAEoE;AACpE,MAAM,UAAU,kBAAkB,CAChC,WAAmB,EACnB,YAAoB,EACpB,YAAoB,EACpB,aAAqB,EACrB,KAAc;IAEd,OAAO,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,KAAK,CACnE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAChD,IAAI,UAAU,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa;QACzE,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;AACtD,CAAC;AASD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,MAAM,CAAC;AAClD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,MAAM,CAAC;AAC7C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAG,MAAM,CAAC;AAC/C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD,MAAM,CAAC,MAAM,qBAAqB,GAChC,iFAAiF;IACjF,8DAA8D,mBAAmB,GAAG;IACpF,GAAG,oBAAoB,4CAA4C;IACnE,IAAI,mBAAmB,IAAI,uBAAuB,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"route-fallback.js","sourceRoot":"","sources":["../src/route-fallback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const REDACTION_VERSION = 2;
|
|
2
|
+
export declare const REDACTED_PREFIX = "[REDACTED:";
|
|
3
|
+
export type RedactionCounts = Record<string, number>;
|
|
4
|
+
export interface RedactOptions {
|
|
5
|
+
/** Run the financial rules (default true, as polychat with MEMTREE_FINANCIAL_REDACTION). */
|
|
6
|
+
financial?: boolean;
|
|
7
|
+
/** The field key the text sits under ({"routing_number": "..."}): financial context. */
|
|
8
|
+
context?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Text with secrets replaced, and counts by kind. Secret-free text is returned as is. */
|
|
11
|
+
export declare function redact(text: string, options?: RedactOptions): {
|
|
12
|
+
text: string;
|
|
13
|
+
counts: RedactionCounts;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Redact every string in a JSON-like value. A string under a secret-named key
|
|
17
|
+
* ({"password": "..."}) is checked as that name's assigned value; every key is the
|
|
18
|
+
* financial rules' context. Only base64 media payloads are kept as they are. Returns the
|
|
19
|
+
* same object when nothing was redacted (so unchanged payloads stay identical).
|
|
20
|
+
*/
|
|
21
|
+
export declare function redactValue<T>(value: T, counts?: RedactionCounts, options?: {
|
|
22
|
+
financial?: boolean;
|
|
23
|
+
}): T;
|
|
24
|
+
export declare function formatCounts(counts: RedactionCounts): string;
|
|
25
|
+
//# sourceMappingURL=secret-redaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-redaction.d.ts","sourceRoot":"","sources":["../src/secret-redaction.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACrD,MAAM,WAAW,aAAa;IAC5B,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+FD,0FAA0F;AAC1F,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAC9D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAM1C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,GAAE,eAAoB,EACtC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,CAAC,CAEvE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAG5D"}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Secret and financial-detail redaction: the TypeScript twin of polychat's
|
|
2
|
+
// polychat/secret_redaction.py (+ financial_redaction.py).
|
|
3
|
+
//
|
|
4
|
+
// redact(text) replaces API keys, tokens, passwords, private keys and (financial: true, the
|
|
5
|
+
// default) card, bank, IBAN and SSN numbers with "[REDACTED:<kind>]" and returns counts by
|
|
6
|
+
// kind. Local, free, no network. The output must match the Python detector byte for byte:
|
|
7
|
+
// both are checked against the shared golden vectors (test/fixtures/
|
|
8
|
+
// secret_redaction_vectors.json, copied from polychat). Change both together.
|
|
9
|
+
//
|
|
10
|
+
// Not wired into uploads yet: ccc sends conversations to Polychat as they are and Polychat
|
|
11
|
+
// redacts what it stores and shows (docs/secret_redaction.md in polychat). This module keeps
|
|
12
|
+
// the client able to redact identically once it does (plan item 28 decides where).
|
|
13
|
+
//
|
|
14
|
+
// Like the Python detector it works on the text's UTF-8 bytes (utf8-bytes.ts), so offsets
|
|
15
|
+
// and byte windows agree. Every pattern is bounded. Text without a secret comes back as the
|
|
16
|
+
// same string, so secret-free payloads (and their hashes) are unchanged.
|
|
17
|
+
import { spansWithContext } from "./financial-redaction.js";
|
|
18
|
+
import { asciiLower, fromBytes, toBytes } from "./utf8-bytes.js";
|
|
19
|
+
// 1: secrets. 2: secrets and financial details (polychat MEMTREE_FINANCIAL_REDACTION on).
|
|
20
|
+
export const REDACTION_VERSION = 2;
|
|
21
|
+
export const REDACTED_PREFIX = "[REDACTED:";
|
|
22
|
+
const FINANCIAL_ROUNDS = 3;
|
|
23
|
+
const TOK = "A-Za-z0-9_\\-";
|
|
24
|
+
const TOK_CHAR = /[A-Za-z0-9_-]/;
|
|
25
|
+
const END = `(?![${TOK}])`;
|
|
26
|
+
const MIN_RUN = 16;
|
|
27
|
+
// Provider tokens matched at the start of a run of 16+ token chars: [kind, prefixes, regex].
|
|
28
|
+
const RUN_TOKENS = [
|
|
29
|
+
["openai_key", ["sk-"], new RegExp(`sk-[${TOK}]{20,400}`, "y")],
|
|
30
|
+
["groq_key", ["gsk_"], new RegExp(`gsk_[A-Za-z0-9]{30,200}${END}`, "y")],
|
|
31
|
+
["xai_key", ["xai-"], new RegExp(`xai-[A-Za-z0-9]{40,200}${END}`, "y")],
|
|
32
|
+
["huggingface_token", ["hf_"], new RegExp(`hf_[A-Za-z0-9]{30,200}${END}`, "y")],
|
|
33
|
+
["replicate_token", ["r8_"], new RegExp(`r8_[A-Za-z0-9]{30,200}${END}`, "y")],
|
|
34
|
+
["perplexity_key", ["pplx-"], new RegExp(`pplx-[A-Za-z0-9]{40,200}${END}`, "y")],
|
|
35
|
+
["voyage_key", ["pa-"], new RegExp(`pa-[${TOK}]{40,200}${END}`, "y")],
|
|
36
|
+
["google_api_key", ["AIza"], new RegExp(`AIza[0-9A-Za-z_\\-]{35}${END}`, "y")],
|
|
37
|
+
["github_token", ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"],
|
|
38
|
+
new RegExp(`gh[pousr]_[A-Za-z0-9]{36,255}${END}`, "y")],
|
|
39
|
+
["github_token", ["github_pat_"], new RegExp(`github_pat_[A-Za-z0-9_]{40,255}${END}`, "y")],
|
|
40
|
+
["gitlab_token", ["glpat-"], new RegExp(`glpat-[${TOK}]{20,100}${END}`, "y")],
|
|
41
|
+
["stripe_key", ["sk_live_", "sk_test_", "rk_live_", "rk_test_"],
|
|
42
|
+
new RegExp(`[sr]k_(?:live|test)_[A-Za-z0-9]{16,255}${END}`, "y")],
|
|
43
|
+
["slack_token", ["xox"], new RegExp(`xox[abposr]-[A-Za-z0-9\\-]{10,255}${END}`, "y")],
|
|
44
|
+
["aws_access_key_id", ["AKIA", "ASIA"], new RegExp(`A[KS]IA[A-Z0-9]{16}${END}`, "y")],
|
|
45
|
+
["jwt", ["eyJ"], new RegExp(`eyJ[${TOK}]{8,4000}\\.eyJ[${TOK}]{8,8000}\\.[${TOK}]{8,2000}`, "y")],
|
|
46
|
+
];
|
|
47
|
+
const RUN_RE = new RegExp(`[${TOK}]{${MIN_RUN},}`, "g");
|
|
48
|
+
const B64 = /[A-Za-z0-9+/=]/;
|
|
49
|
+
// Tokens with non-token chars early on: [kind, trigger, pattern, chars that continue its run].
|
|
50
|
+
const SCAN_TOKENS = [
|
|
51
|
+
["stripe_webhook_secret", "whsec_", /whsec_[A-Za-z0-9+/=]{24,255}/g, B64],
|
|
52
|
+
["slack_webhook", "hooks.slack.com",
|
|
53
|
+
/hooks\.slack\.com\/services\/T[A-Za-z0-9_]{6,20}\/B[A-Za-z0-9_]{6,20}\/[A-Za-z0-9_]{16,64}/g,
|
|
54
|
+
/[A-Za-z0-9_]/],
|
|
55
|
+
["sendgrid_key", "SG.", new RegExp(`SG\\.[${TOK}]{16,32}\\.[${TOK}]{16,64}${END}`, "g"),
|
|
56
|
+
TOK_CHAR],
|
|
57
|
+
];
|
|
58
|
+
const SK_KINDS = [
|
|
59
|
+
["sk-ant-", "anthropic_key"], ["sk-or-", "openrouter_key"], ["sk-nano-", "nanogpt_key"],
|
|
60
|
+
];
|
|
61
|
+
// Byte-string classes: \x7f-\xff is every non-ASCII byte.
|
|
62
|
+
const URL_PASSWORD = new RegExp("://[^\\x00-\\x20\\x7f-\\xff:/@\"'<>\\\\]{0,128}:"
|
|
63
|
+
+ "([^\\x00-\\x20\\x7f-\\xff/@\"'<>\\\\]{1,256})@(?=[A-Za-z0-9\\[])", "g");
|
|
64
|
+
const BEARERS = ["Bearer", "bearer", "BEARER"].map((w) => new RegExp(`${w}[ \\t]{1,4}([A-Za-z0-9._~+/=\\-]{16,2000})`, "g"));
|
|
65
|
+
const BASIC_AUTHS = ["Authorization", "authorization"].map((w) => new RegExp(`${w}["']?[ \\t]{0,3}[:=][ \\t]{0,3}["']?[Bb]asic[ \\t]{1,4}([A-Za-z0-9+/=]{8,1000})`, "g"));
|
|
66
|
+
// Name roots, found in an ASCII-lowercased copy (overlapping hits kept).
|
|
67
|
+
const NAME_HIT = /pass|pwd|secret|token|key|auth|credential/g;
|
|
68
|
+
const ASSIGN_TAIL = new RegExp("([A-Za-z0-9_\\-]{0,40})[\"']?[ \\t]{0,3}(?::=|=>|[:=])[ \\t]{0,3}"
|
|
69
|
+
+ "([\"'`]?)([\\x21-\\x7e]{1,300})", "y");
|
|
70
|
+
const VALUE_STOP = /["'`<>,;&()[\]{}\\]/;
|
|
71
|
+
const KEY_QUALIFIED = /(?:api|access|secret|private|signing|encryption|master|account|service|auth|client|license|admin|app)[_-]?key/;
|
|
72
|
+
const NON_SECRET_NAME_ENDS = [
|
|
73
|
+
"hash", "_id", "_len", "_length", "_count", "_type", "_name", "_path", "_file", "_url",
|
|
74
|
+
"_uri", "_endpoint", "_env", "_var", "_header", "_prefix", "_field", "_min", "_max",
|
|
75
|
+
"_size", "_limit", "_ttl", "_expiry", "_at", "_secs", "_seconds", "_version", "_dir",
|
|
76
|
+
];
|
|
77
|
+
const PEM_BEGIN = /-----BEGIN (?:[A-Z0-9]{1,12} ){0,3}PRIVATE KEY(?: BLOCK)?-----/g;
|
|
78
|
+
const PEM_END = /-----END (?:[A-Z0-9]{1,12} ){0,3}PRIVATE KEY(?: BLOCK)?-----/;
|
|
79
|
+
const PEM_BODY = /(?:\r?\n|\\n)?(?:[A-Za-z0-9+/=]{1,200}(?:\r?\n|\\n)?){0,400}/y;
|
|
80
|
+
const PEM_MAX = 20000;
|
|
81
|
+
const PEM_MIN_BODY = 64;
|
|
82
|
+
// Each newline with the one byte before it when that byte decides whether it is a line
|
|
83
|
+
// break for polychat's splitter (?<!\\)\r?\n: kept exactly so line addresses never move.
|
|
84
|
+
const LINE_BREAK = /[\\\r]?\n/g;
|
|
85
|
+
const PLACEHOLDER_WORDS = new Set([
|
|
86
|
+
"none", "null", "nil", "true", "false", "undefined", "required", "optional", "string",
|
|
87
|
+
"str", "int", "bool", "env", "changeme", "password", "secret", "token", "example",
|
|
88
|
+
"redacted", "your_api_key", "xxx", "todo", "dummy", "test", "default",
|
|
89
|
+
]);
|
|
90
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
91
|
+
// YOUR_PASSWORD_HERE, db_password, my-db-pass: names and placeholders, not passwords.
|
|
92
|
+
const WORDS_IDENT = /^(?:[A-Z]+(?:[_-][A-Z]+)+|[a-z]+(?:[_-][a-z]+)+)$/;
|
|
93
|
+
const DOTTED_IDENT = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/;
|
|
94
|
+
const MIN_PASSWORD_ENTROPY = 2.0;
|
|
95
|
+
// Structured fields ({"password": "..."}): an identifier key and one printable-ASCII word.
|
|
96
|
+
const FIELD_NAME = /^[A-Za-z0-9_-]{1,80}$/;
|
|
97
|
+
const FIELD_VALUE = /^[\x21-\x7e]{1,300}$/;
|
|
98
|
+
const BINARY_PAYLOAD_KEYS = new Set(["data", "file_data"]);
|
|
99
|
+
const DATA_URI_KEYS = new Set(["url", "image_url", "file_data", "data"]);
|
|
100
|
+
const BASE64_TEXT = /^[A-Za-z0-9+/=\r\n]{16,}$/;
|
|
101
|
+
const BASE64_DATA_URI = /^data:[A-Za-z0-9.+-]{1,64}\/[A-Za-z0-9.+-]{1,128}(?:;[A-Za-z0-9.+-]{1,64}=[A-Za-z0-9.+-]{1,128}){0,4};base64,/;
|
|
102
|
+
const MEDIA_TYPES = new Set(["image", "image_url", "input_image", "document", "file"]);
|
|
103
|
+
/** Text with secrets replaced, and counts by kind. Secret-free text is returned as is. */
|
|
104
|
+
export function redact(text, options = {}) {
|
|
105
|
+
if (typeof text !== "string" || text.length === 0)
|
|
106
|
+
return { text, counts: {} };
|
|
107
|
+
const counts = {};
|
|
108
|
+
const out = redactBytes(toBytes(text), counts, options.financial ?? true, toBytes(options.context ?? ""));
|
|
109
|
+
return Object.keys(counts).length ? { text: fromBytes(out), counts } : { text, counts: {} };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Redact every string in a JSON-like value. A string under a secret-named key
|
|
113
|
+
* ({"password": "..."}) is checked as that name's assigned value; every key is the
|
|
114
|
+
* financial rules' context. Only base64 media payloads are kept as they are. Returns the
|
|
115
|
+
* same object when nothing was redacted (so unchanged payloads stay identical).
|
|
116
|
+
*/
|
|
117
|
+
export function redactValue(value, counts = {}, options = {}) {
|
|
118
|
+
return walk(value, counts, null, false, options.financial ?? true);
|
|
119
|
+
}
|
|
120
|
+
export function formatCounts(counts) {
|
|
121
|
+
const keys = Object.keys(counts).sort();
|
|
122
|
+
return keys.length ? keys.map((k) => `${k}=${counts[k]}`).join(", ") : "none";
|
|
123
|
+
}
|
|
124
|
+
function marker(kind) {
|
|
125
|
+
return `${REDACTED_PREFIX}${kind}]`;
|
|
126
|
+
}
|
|
127
|
+
function redactBytes(data, counts, financial, context) {
|
|
128
|
+
let out = apply(data, pemSpans(data), counts);
|
|
129
|
+
out = apply(out, tokenSpans(out), counts);
|
|
130
|
+
if (out.includes("://"))
|
|
131
|
+
out = apply(out, urlSpans(out), counts);
|
|
132
|
+
out = apply(out, headerSpans(out), counts);
|
|
133
|
+
out = apply(out, assignmentSpans(out), counts);
|
|
134
|
+
if (!financial)
|
|
135
|
+
return out;
|
|
136
|
+
// A marker shorter than its value moves context words closer: repeat until stable.
|
|
137
|
+
for (let round = 0; round < FINANCIAL_ROUNDS; round += 1) {
|
|
138
|
+
const spans = spansWithContext(out, context);
|
|
139
|
+
if (spans.length === 0)
|
|
140
|
+
break;
|
|
141
|
+
out = apply(out, spans, counts);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
// --------------------------------------------------------------------------- values
|
|
146
|
+
function walk(value, counts, key, media, financial) {
|
|
147
|
+
if (typeof value === "string") {
|
|
148
|
+
const result = redactField(key, value, financial);
|
|
149
|
+
for (const [k, n] of Object.entries(result.counts))
|
|
150
|
+
counts[k] = (counts[k] ?? 0) + n;
|
|
151
|
+
return result.text;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
return mapIfChanged(value, (v) => walk(v, counts, key, media, financial));
|
|
155
|
+
}
|
|
156
|
+
if (isRecord(value)) {
|
|
157
|
+
const inner = media || isBinaryPart(value);
|
|
158
|
+
let out = null;
|
|
159
|
+
for (const [k, item] of Object.entries(value)) {
|
|
160
|
+
const next = isBinaryPayload(value, k, item, inner)
|
|
161
|
+
? item : walk(item, counts, k, inner, financial);
|
|
162
|
+
if (next !== item && out === null)
|
|
163
|
+
out = { ...value };
|
|
164
|
+
if (out !== null)
|
|
165
|
+
out[k] = next;
|
|
166
|
+
}
|
|
167
|
+
return out ?? value;
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
/** A string value, redacted whole when its key names a secret and it looks like one. */
|
|
172
|
+
function redactField(key, text, financial) {
|
|
173
|
+
const kind = key ? fieldSecretKind(key, text) : null;
|
|
174
|
+
if (kind !== null)
|
|
175
|
+
return { text: marker(kind), counts: { [kind]: 1 } };
|
|
176
|
+
return redact(text, { financial, context: key ?? "" });
|
|
177
|
+
}
|
|
178
|
+
function fieldSecretKind(key, value) {
|
|
179
|
+
if (!FIELD_NAME.test(key) || !FIELD_VALUE.test(value))
|
|
180
|
+
return null;
|
|
181
|
+
const kind = secretNameKind(key);
|
|
182
|
+
if (kind === null)
|
|
183
|
+
return null;
|
|
184
|
+
const ok = kind === "password" ? plausiblePassword(value) : plausibleSecret(value);
|
|
185
|
+
return ok ? kind : null;
|
|
186
|
+
}
|
|
187
|
+
/** parent[key] is a base64 media payload inside a media part. */
|
|
188
|
+
function isBinaryPayload(parent, key, value, media) {
|
|
189
|
+
if (!media || typeof value !== "string")
|
|
190
|
+
return false;
|
|
191
|
+
if (DATA_URI_KEYS.has(key) && value.startsWith("data:")) {
|
|
192
|
+
const head = BASE64_DATA_URI.exec(value);
|
|
193
|
+
return head !== null && BASE64_TEXT.test(value.slice(head[0].length));
|
|
194
|
+
}
|
|
195
|
+
const kind = pyTruthy(parent.media_type) ? parent.media_type : parent.mime_type;
|
|
196
|
+
const declared = parent.type === "base64"
|
|
197
|
+
|| (typeof kind === "string" && !kind.startsWith("text/"));
|
|
198
|
+
return declared && BINARY_PAYLOAD_KEYS.has(key) && BASE64_TEXT.test(value);
|
|
199
|
+
}
|
|
200
|
+
function isBinaryPart(part) {
|
|
201
|
+
if (MEDIA_TYPES.has(part.type))
|
|
202
|
+
return true;
|
|
203
|
+
const source = part.source;
|
|
204
|
+
return "image_url" in part || (isRecord(source) && source.type === "base64");
|
|
205
|
+
}
|
|
206
|
+
// --------------------------------------------------------------------------- passes
|
|
207
|
+
function pemSpans(data) {
|
|
208
|
+
if (!data.includes("PRIVATE KEY"))
|
|
209
|
+
return [];
|
|
210
|
+
const spans = [];
|
|
211
|
+
PEM_BEGIN.lastIndex = 0;
|
|
212
|
+
let begin;
|
|
213
|
+
while ((begin = PEM_BEGIN.exec(data)) !== null) {
|
|
214
|
+
const bodyStart = begin.index + begin[0].length;
|
|
215
|
+
const window = data.slice(bodyStart, Math.min(data.length, bodyStart + PEM_MAX));
|
|
216
|
+
const end = PEM_END.exec(window);
|
|
217
|
+
let stop;
|
|
218
|
+
let body;
|
|
219
|
+
if (end) {
|
|
220
|
+
stop = bodyStart + end.index + end[0].length;
|
|
221
|
+
body = window.slice(0, end.index);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
PEM_BODY.lastIndex = bodyStart;
|
|
225
|
+
stop = bodyStart + (PEM_BODY.exec(data)?.[0].length ?? 0);
|
|
226
|
+
body = data.slice(bodyStart, stop);
|
|
227
|
+
}
|
|
228
|
+
const real = base64Chars(body) >= PEM_MIN_BODY; // a bare header or "..." is a mention
|
|
229
|
+
if (real)
|
|
230
|
+
spans.push([begin.index, stop, "private_key"]);
|
|
231
|
+
PEM_BEGIN.lastIndex = real ? stop : bodyStart;
|
|
232
|
+
}
|
|
233
|
+
return spans;
|
|
234
|
+
}
|
|
235
|
+
function tokenSpans(data) {
|
|
236
|
+
const spans = [];
|
|
237
|
+
for (const run of data.matchAll(RUN_RE)) {
|
|
238
|
+
const span = runTokenSpan(data, run.index);
|
|
239
|
+
if (span)
|
|
240
|
+
spans.push(span);
|
|
241
|
+
}
|
|
242
|
+
for (const [kind, trigger, pattern, chars] of SCAN_TOKENS) {
|
|
243
|
+
if (!data.includes(trigger))
|
|
244
|
+
continue;
|
|
245
|
+
for (const m of data.matchAll(pattern)) {
|
|
246
|
+
if (!insideToken(data, m.index) && plausibleToken(m[0])) {
|
|
247
|
+
spans.push([m.index, runEnd(data, m.index + m[0].length, chars), kind]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return nonOverlapping(spans);
|
|
252
|
+
}
|
|
253
|
+
function runTokenSpan(data, start) {
|
|
254
|
+
for (const [kind, prefixes, pattern] of RUN_TOKENS) {
|
|
255
|
+
if (!prefixes.some((p) => data.startsWith(p, start)))
|
|
256
|
+
continue;
|
|
257
|
+
pattern.lastIndex = start;
|
|
258
|
+
const m = pattern.exec(data);
|
|
259
|
+
if (m && plausibleToken(m[0])) {
|
|
260
|
+
// The whole run: a capped match must not leave a tail that a second pass reads as a
|
|
261
|
+
// fresh token.
|
|
262
|
+
return [start, runEnd(data, start + m[0].length, TOK_CHAR), tokenKind(kind, m[0])];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
/** end moved past any further chars matching `chars` (the rest of a token run). */
|
|
268
|
+
function runEnd(data, end, chars) {
|
|
269
|
+
let pos = end;
|
|
270
|
+
while (pos < data.length && chars.test(data[pos]))
|
|
271
|
+
pos += 1;
|
|
272
|
+
return pos;
|
|
273
|
+
}
|
|
274
|
+
function urlSpans(data) {
|
|
275
|
+
const spans = [];
|
|
276
|
+
for (const m of data.matchAll(URL_PASSWORD)) {
|
|
277
|
+
const start = m.index + m[0].length - 1 - m[1].length;
|
|
278
|
+
if (plausiblePassword(m[1]))
|
|
279
|
+
spans.push([start, start + m[1].length, "url_password"]);
|
|
280
|
+
}
|
|
281
|
+
return spans;
|
|
282
|
+
}
|
|
283
|
+
function headerSpans(data) {
|
|
284
|
+
const spans = [];
|
|
285
|
+
const groups = [
|
|
286
|
+
["basic_auth", BASIC_AUTHS, () => true, "uthorization"],
|
|
287
|
+
["bearer_token", BEARERS, plausibleToken, "earer"],
|
|
288
|
+
];
|
|
289
|
+
for (const [kind, patterns, accept, trigger] of groups) {
|
|
290
|
+
if (!data.includes(trigger) && !data.includes(trigger.toUpperCase()))
|
|
291
|
+
continue;
|
|
292
|
+
for (const pattern of patterns) {
|
|
293
|
+
for (const m of data.matchAll(pattern)) {
|
|
294
|
+
if (insideToken(data, m.index) || !accept(m[1]))
|
|
295
|
+
continue;
|
|
296
|
+
const start = m.index + m[0].length - m[1].length;
|
|
297
|
+
spans.push([start, start + m[1].length, kind]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return nonOverlapping(spans);
|
|
302
|
+
}
|
|
303
|
+
function assignmentSpans(data) {
|
|
304
|
+
const spans = [];
|
|
305
|
+
let skipTo = 0;
|
|
306
|
+
for (const hit of nameHits(data)) {
|
|
307
|
+
if (hit < skipTo)
|
|
308
|
+
continue;
|
|
309
|
+
ASSIGN_TAIL.lastIndex = hit;
|
|
310
|
+
const m = ASSIGN_TAIL.exec(data);
|
|
311
|
+
if (!m)
|
|
312
|
+
continue;
|
|
313
|
+
skipTo = hit + m[1].length;
|
|
314
|
+
const span = assignmentSpan(data, hit, m);
|
|
315
|
+
if (span) {
|
|
316
|
+
spans.push(span);
|
|
317
|
+
skipTo = span[1];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return spans;
|
|
321
|
+
}
|
|
322
|
+
function nameHits(data) {
|
|
323
|
+
const low = asciiLower(data);
|
|
324
|
+
const hits = new Set();
|
|
325
|
+
// Overlapping hits ("passwd" inside "pass..."): restart one char after each match start.
|
|
326
|
+
NAME_HIT.lastIndex = 0;
|
|
327
|
+
let m;
|
|
328
|
+
while ((m = NAME_HIT.exec(low)) !== null) {
|
|
329
|
+
hits.add(m.index);
|
|
330
|
+
NAME_HIT.lastIndex = m.index + 1;
|
|
331
|
+
}
|
|
332
|
+
return [...hits].sort((a, b) => a - b);
|
|
333
|
+
}
|
|
334
|
+
function assignmentSpan(data, hit, m) {
|
|
335
|
+
const kind = secretNameKind(data.slice(identStart(data, hit), hit) + m[1]);
|
|
336
|
+
if (kind === null)
|
|
337
|
+
return null;
|
|
338
|
+
const raw = m[3];
|
|
339
|
+
const valueStart = m.index + m[0].length - raw.length;
|
|
340
|
+
const stop = VALUE_STOP.exec(raw);
|
|
341
|
+
const value = stop ? raw.slice(0, stop.index) : raw;
|
|
342
|
+
if (!m[2] && stop && "([{".includes(raw[value.length]))
|
|
343
|
+
return null; // f(x): code
|
|
344
|
+
const ok = kind === "password" ? plausiblePassword(value) : plausibleSecret(value);
|
|
345
|
+
return ok ? [valueStart, valueStart + value.length, kind] : null;
|
|
346
|
+
}
|
|
347
|
+
function apply(data, spans, counts) {
|
|
348
|
+
if (spans.length === 0)
|
|
349
|
+
return data;
|
|
350
|
+
const out = [];
|
|
351
|
+
let pos = 0;
|
|
352
|
+
for (const [start, end, kind] of [...spans].sort(compareSpans)) {
|
|
353
|
+
if (start < pos)
|
|
354
|
+
continue;
|
|
355
|
+
out.push(data.slice(pos, start));
|
|
356
|
+
// Keep the span's line breaks exactly so line addresses never move.
|
|
357
|
+
out.push(marker(kind) + (data.slice(start, end).match(LINE_BREAK) ?? []).join(""));
|
|
358
|
+
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
359
|
+
pos = end;
|
|
360
|
+
}
|
|
361
|
+
out.push(data.slice(pos));
|
|
362
|
+
return out.join("");
|
|
363
|
+
}
|
|
364
|
+
function nonOverlapping(spans) {
|
|
365
|
+
const kept = [];
|
|
366
|
+
for (const span of [...spans].sort(compareSpans)) {
|
|
367
|
+
if (kept.length === 0 || span[0] >= kept[kept.length - 1][1])
|
|
368
|
+
kept.push(span);
|
|
369
|
+
}
|
|
370
|
+
return kept;
|
|
371
|
+
}
|
|
372
|
+
function compareSpans(a, b) {
|
|
373
|
+
return a[0] - b[0] || a[1] - b[1] || (a[2] < b[2] ? -1 : a[2] > b[2] ? 1 : 0);
|
|
374
|
+
}
|
|
375
|
+
function tokenKind(kind, token) {
|
|
376
|
+
if (kind !== "openai_key")
|
|
377
|
+
return kind;
|
|
378
|
+
for (const [prefix, skKind] of SK_KINDS)
|
|
379
|
+
if (token.startsWith(prefix))
|
|
380
|
+
return skKind;
|
|
381
|
+
return kind;
|
|
382
|
+
}
|
|
383
|
+
function identStart(data, pos) {
|
|
384
|
+
let start = pos;
|
|
385
|
+
const floor = Math.max(0, pos - 64);
|
|
386
|
+
while (start > floor && TOK_CHAR.test(data[start - 1]))
|
|
387
|
+
start -= 1;
|
|
388
|
+
return start;
|
|
389
|
+
}
|
|
390
|
+
function insideToken(data, pos) {
|
|
391
|
+
return pos > 0 && TOK_CHAR.test(data[pos - 1]);
|
|
392
|
+
}
|
|
393
|
+
function secretNameKind(name) {
|
|
394
|
+
const low = asciiLower(name);
|
|
395
|
+
if (NON_SECRET_NAME_ENDS.some((e) => low.endsWith(e)))
|
|
396
|
+
return null;
|
|
397
|
+
if (["passw", "passphrase", "pwd"].some((w) => low.includes(w)) || low === "pass"
|
|
398
|
+
|| low.endsWith("_pass") || low.endsWith("-pass"))
|
|
399
|
+
return "password";
|
|
400
|
+
if (low.includes("secret") || low.includes("token") || low.includes("credential")) {
|
|
401
|
+
return "secret";
|
|
402
|
+
}
|
|
403
|
+
if (low.includes("auth") && !low.includes("author"))
|
|
404
|
+
return "secret";
|
|
405
|
+
if (KEY_QUALIFIED.test(low) || (/[A-Z]/.test(name) && !/[a-z]/.test(name)
|
|
406
|
+
&& name.endsWith("KEY")))
|
|
407
|
+
return "secret";
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
// --------------------------------------------------------------------------- value checks
|
|
411
|
+
function plausibleToken(value) {
|
|
412
|
+
if (value.startsWith(REDACTED_PREFIX))
|
|
413
|
+
return false;
|
|
414
|
+
return /[0-9]/.test(value) && /[A-Za-z]/.test(value);
|
|
415
|
+
}
|
|
416
|
+
function plausiblePassword(value) {
|
|
417
|
+
if (isPlaceholder(value) || value.length < 6 || entropy(value) < MIN_PASSWORD_ENTROPY) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
if (DOTTED_IDENT.test(value) || WORDS_IDENT.test(value))
|
|
421
|
+
return false;
|
|
422
|
+
if (value.length >= 12)
|
|
423
|
+
return true;
|
|
424
|
+
return charClasses(value) >= 2 && !/^[0-9]+$/.test(value);
|
|
425
|
+
}
|
|
426
|
+
function plausibleSecret(value) {
|
|
427
|
+
if (isPlaceholder(value) || value.length < 16)
|
|
428
|
+
return false;
|
|
429
|
+
if (DOTTED_IDENT.test(value))
|
|
430
|
+
return false;
|
|
431
|
+
if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value))
|
|
432
|
+
return false;
|
|
433
|
+
return HEX.test(value) || entropy(value) >= 3.0;
|
|
434
|
+
}
|
|
435
|
+
function isPlaceholder(value) {
|
|
436
|
+
if (!value || "$<{%*#@[".includes(value[0]) || value.startsWith(REDACTED_PREFIX))
|
|
437
|
+
return true;
|
|
438
|
+
const low = asciiLower(value);
|
|
439
|
+
if (PLACEHOLDER_WORDS.has(low) || value.includes("...") || low.includes("xxxx"))
|
|
440
|
+
return true;
|
|
441
|
+
return new Set(value).size <= 2;
|
|
442
|
+
}
|
|
443
|
+
function charClasses(value) {
|
|
444
|
+
return Number(/[a-z]/.test(value)) + Number(/[A-Z]/.test(value))
|
|
445
|
+
+ Number(/[0-9]/.test(value)) + Number(/[^A-Za-z0-9]/.test(value));
|
|
446
|
+
}
|
|
447
|
+
// Python's sum() of floats is Neumaier-compensated (3.12+); match it so thresholds agree.
|
|
448
|
+
function entropy(value) {
|
|
449
|
+
const n = value.length;
|
|
450
|
+
const freq = new Map();
|
|
451
|
+
for (const c of value)
|
|
452
|
+
freq.set(c, (freq.get(c) ?? 0) + 1);
|
|
453
|
+
let sum = 0;
|
|
454
|
+
let comp = 0;
|
|
455
|
+
for (const count of freq.values()) {
|
|
456
|
+
const term = (count / n) * Math.log2(count / n);
|
|
457
|
+
const t = sum + term;
|
|
458
|
+
comp += Math.abs(sum) >= Math.abs(term) ? (sum - t) + term : (term - t) + sum;
|
|
459
|
+
sum = t;
|
|
460
|
+
}
|
|
461
|
+
return -(sum + comp);
|
|
462
|
+
}
|
|
463
|
+
function base64Chars(body) {
|
|
464
|
+
let n = 0;
|
|
465
|
+
for (let i = 0; i < body.length; i += 1)
|
|
466
|
+
if (B64.test(body[i]))
|
|
467
|
+
n += 1;
|
|
468
|
+
return n;
|
|
469
|
+
}
|
|
470
|
+
/** Python's bool(value) for JSON values: null, false, 0, "", [] and {} are false. */
|
|
471
|
+
function pyTruthy(value) {
|
|
472
|
+
if (Array.isArray(value))
|
|
473
|
+
return value.length > 0;
|
|
474
|
+
if (isRecord(value))
|
|
475
|
+
return Object.keys(value).length > 0;
|
|
476
|
+
return Boolean(value);
|
|
477
|
+
}
|
|
478
|
+
function isRecord(value) {
|
|
479
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
|
+
}
|
|
481
|
+
function mapIfChanged(items, fn) {
|
|
482
|
+
let out = null;
|
|
483
|
+
items.forEach((item, i) => {
|
|
484
|
+
const next = fn(item);
|
|
485
|
+
if (next !== item && out === null)
|
|
486
|
+
out = items.slice(0, i);
|
|
487
|
+
if (out !== null)
|
|
488
|
+
out.push(next);
|
|
489
|
+
});
|
|
490
|
+
return out ?? items;
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=secret-redaction.js.map
|