@code-yeongyu/senpi-ai 2026.10.26-2 → 2026.10.27-2
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/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/azure-openai-responses.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +12 -1
- package/dist/utils/assistant-capture-record.d.ts +13 -0
- package/dist/utils/assistant-capture-record.js +163 -0
- package/dist/utils/assistant-capture.d.ts +10 -4
- package/dist/utils/assistant-capture.js +2 -159
- package/dist/utils/assistant-seed-frontier.d.ts +7 -0
- package/dist/utils/assistant-seed-frontier.js +14 -0
- package/dist/utils/event-stream.d.ts +1 -0
- package/dist/utils/event-stream.js +13 -0
- package/dist/utils/provider-error.d.ts +12 -0
- package/dist/utils/provider-error.js +28 -0
- package/dist/utils/retry.js +3 -0
- package/dist/utils/secret-classifier.d.ts +28 -0
- package/dist/utils/secret-classifier.js +258 -0
- package/dist/utils/secret-lexer.d.ts +51 -0
- package/dist/utils/secret-lexer.js +295 -0
- package/dist/utils/secret-redaction.d.ts +13 -0
- package/dist/utils/secret-redaction.js +198 -0
- package/package.json +2 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Credential redaction for provider and gateway text, read token by token in one pass (senpi#4283).
|
|
2
|
+
//
|
|
3
|
+
// The text is read ONCE, left to right, as alternating runs: a token (value characters) and a delimiter (everything
|
|
4
|
+
// between two tokens), see secret-lexer.ts. A token is classified only from its own characters and the delimiters on
|
|
5
|
+
// either side of it (secret-classifier.ts); nothing inside the loop searches, slices from the start or re-reads earlier
|
|
6
|
+
// text, so the work is linear in the input whatever its shape. The scanner uses no regular expressions. A replaced
|
|
7
|
+
// range is a whole token (plus the base64 padding after it), the rest of a token from where a credential shape starts,
|
|
8
|
+
// the whole value of a strict name (to its closing quote, or to the end of a bare value; the scan continues after it),
|
|
9
|
+
// or a URL userinfo password, from its ":" to the "@".
|
|
10
|
+
import { COOKIE_ATTRIBUTES, COOKIE_WORDS, credentialShapeEnd, credentialShapeStart, isInert, lowerToken, nameStart, SCHEMES, STRICT_WORDS, secretWord, } from "./secret-classifier.js";
|
|
11
|
+
import { bareValueEnd, emptyDelimiter, quotedValueEnd, readDelimiter, readUnit, tokenUnitLength, } from "./secret-lexer.js";
|
|
12
|
+
const MIN_SECRET_LENGTH = 16;
|
|
13
|
+
const MIN_BEARER_LENGTH = 8;
|
|
14
|
+
const OBJECT_SPAN = 4096;
|
|
15
|
+
const MAX_COOKIE_PAIRS = 64;
|
|
16
|
+
const MAX_USERINFO_LENGTH = 256;
|
|
17
|
+
/** Append [start, end) to the sorted, disjoint ranges, merging what it overlaps. */
|
|
18
|
+
function addRange(ranges, start, end) {
|
|
19
|
+
let stop = end;
|
|
20
|
+
while (ranges.length > 0 && (ranges[ranges.length - 2] ?? 0) >= start) {
|
|
21
|
+
stop = Math.max(stop, ranges[ranges.length - 1] ?? 0);
|
|
22
|
+
ranges.length -= 2;
|
|
23
|
+
}
|
|
24
|
+
if (ranges.length > 0 && (ranges[ranges.length - 1] ?? 0) >= start) {
|
|
25
|
+
ranges[ranges.length - 1] = Math.max(ranges[ranges.length - 1] ?? 0, stop);
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
ranges.push(start, stop);
|
|
29
|
+
}
|
|
30
|
+
/** The [start, end) ranges of every credential in text, found in one left-to-right pass. */
|
|
31
|
+
function secretRanges(text, shapesOnly = false) {
|
|
32
|
+
const ranges = [];
|
|
33
|
+
let before = emptyDelimiter();
|
|
34
|
+
let after = emptyDelimiter();
|
|
35
|
+
readDelimiter(text, 0, before);
|
|
36
|
+
if (before.refusalStart >= 0)
|
|
37
|
+
return [before.refusalStart, text.length];
|
|
38
|
+
let i = before.end;
|
|
39
|
+
let mode = "none";
|
|
40
|
+
let strict = false;
|
|
41
|
+
let quote = -1;
|
|
42
|
+
let quoteDepth = 0;
|
|
43
|
+
let cookiePairs = 0;
|
|
44
|
+
let cookieAttribute = false;
|
|
45
|
+
let objectDepth = 0;
|
|
46
|
+
let objectEnd = 0;
|
|
47
|
+
let bearerArmed = false;
|
|
48
|
+
let userinfoStart = -1;
|
|
49
|
+
while (i < text.length) {
|
|
50
|
+
const start = i;
|
|
51
|
+
let slashes = 0;
|
|
52
|
+
while (i < text.length) {
|
|
53
|
+
const length = tokenUnitLength(text, i);
|
|
54
|
+
if (length === 0)
|
|
55
|
+
break;
|
|
56
|
+
if (readUnit(text, i).character === 47)
|
|
57
|
+
slashes++;
|
|
58
|
+
i += length;
|
|
59
|
+
}
|
|
60
|
+
const end = i;
|
|
61
|
+
readDelimiter(text, end, after);
|
|
62
|
+
i = after.end;
|
|
63
|
+
const valueEnd = after.paddingEnd;
|
|
64
|
+
const valueLength = valueEnd - start;
|
|
65
|
+
if (after.refusalStart >= 0) {
|
|
66
|
+
// Do not let a bounded decoder refusal expose either an earlier shape or an unknown named value.
|
|
67
|
+
addRange(ranges, start, text.length);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (!shapesOnly && userinfoStart >= 0) {
|
|
71
|
+
if (slashes > 0 || end - userinfoStart > MAX_USERINFO_LENGTH)
|
|
72
|
+
userinfoStart = -1;
|
|
73
|
+
else if (after.at >= 0) {
|
|
74
|
+
if (after.at > userinfoStart)
|
|
75
|
+
addRange(ranges, userinfoStart, after.at);
|
|
76
|
+
userinfoStart = -1;
|
|
77
|
+
}
|
|
78
|
+
else if (after.endsUserinfo)
|
|
79
|
+
userinfoStart = -1;
|
|
80
|
+
}
|
|
81
|
+
const first = readUnit(text, start);
|
|
82
|
+
const authority = slashes === 2 && first.character === 47 && readUnit(text, start + first.length).character === 47;
|
|
83
|
+
if (!shapesOnly && authority && before.colonOnly && after.colonOnly)
|
|
84
|
+
userinfoStart = after.end;
|
|
85
|
+
const shape = credentialShapeStart(text, start, end);
|
|
86
|
+
if (shape >= 0) {
|
|
87
|
+
// A second prefix can begin inside the first alphabet, not just after its end. Keep the old conservative
|
|
88
|
+
// token span for joined credentials; only an unambiguous single key releases its filename suffix.
|
|
89
|
+
const joined = credentialShapeStart(text, shape + readUnit(text, shape).length, end, true);
|
|
90
|
+
const stop = joined >= 0 ? end : credentialShapeEnd(text, shape, end);
|
|
91
|
+
addRange(ranges, shape, stop);
|
|
92
|
+
}
|
|
93
|
+
if (shapesOnly)
|
|
94
|
+
continue;
|
|
95
|
+
if (bearerArmed && valueLength >= MIN_BEARER_LENGTH && !isInert(text, start, end)) {
|
|
96
|
+
addRange(ranges, start, valueEnd);
|
|
97
|
+
}
|
|
98
|
+
bearerArmed = after.spaceOnly && lowerToken(text, start, end, 6) === "bearer";
|
|
99
|
+
const idle = mode === "none";
|
|
100
|
+
if (mode === "scheme" && SCHEMES.has(lowerToken(text, start, end, 6) ?? "")) {
|
|
101
|
+
// An auth scheme is never the value itself: the token after it is, or there is none ("Bearer [REDACTED]").
|
|
102
|
+
mode = after.spaceOnly ? "value" : "none";
|
|
103
|
+
}
|
|
104
|
+
else if ((mode === "scheme" || mode === "value") && strict) {
|
|
105
|
+
if (!isInert(text, start, end)) {
|
|
106
|
+
// The whole value of a strict name, read forward to where the field ends; the scan resumes there.
|
|
107
|
+
const stop = quote >= 0 ? quotedValueEnd(text, end, quote, quoteDepth) : bareValueEnd(text, end);
|
|
108
|
+
addRange(ranges, start, Math.max(stop, valueEnd));
|
|
109
|
+
if (stop > end) {
|
|
110
|
+
readDelimiter(text, stop, after);
|
|
111
|
+
i = after.end;
|
|
112
|
+
bearerArmed = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
mode = "none";
|
|
116
|
+
}
|
|
117
|
+
else if (mode === "scheme" || mode === "value") {
|
|
118
|
+
if (valueLength >= MIN_SECRET_LENGTH && !isInert(text, start, end))
|
|
119
|
+
addRange(ranges, start, valueEnd);
|
|
120
|
+
mode = "none";
|
|
121
|
+
}
|
|
122
|
+
else if (mode === "cookie") {
|
|
123
|
+
if (after.pairEquals) {
|
|
124
|
+
cookieAttribute = COOKIE_ATTRIBUTES.has(lowerToken(text, start, end, 11) ?? "");
|
|
125
|
+
mode = "cookie-value";
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (valueLength >= MIN_SECRET_LENGTH && !isInert(text, start, end))
|
|
129
|
+
addRange(ranges, start, valueEnd);
|
|
130
|
+
mode = after.cookieGap && ++cookiePairs < MAX_COOKIE_PAIRS ? "cookie" : "none";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (mode === "cookie-value") {
|
|
134
|
+
if (!cookieAttribute && !isInert(text, start, end))
|
|
135
|
+
addRange(ranges, start, valueEnd);
|
|
136
|
+
mode = after.cookieGap && ++cookiePairs < MAX_COOKIE_PAIRS ? "cookie" : "none";
|
|
137
|
+
}
|
|
138
|
+
else if (mode === "object") {
|
|
139
|
+
const value = before.separator && !before.opensObject;
|
|
140
|
+
if (value && valueLength >= MIN_SECRET_LENGTH && !isInert(text, start, end))
|
|
141
|
+
addRange(ranges, start, valueEnd);
|
|
142
|
+
objectDepth += after.braceDepth;
|
|
143
|
+
if (objectDepth <= 0 || end > objectEnd)
|
|
144
|
+
mode = "none";
|
|
145
|
+
}
|
|
146
|
+
if (idle && (after.separator || (before.endsWithLt && after.closesTag))) {
|
|
147
|
+
const word = secretWord(text, nameStart(text, start, end), end);
|
|
148
|
+
if (word !== undefined && COOKIE_WORDS.has(word)) {
|
|
149
|
+
mode = "cookie";
|
|
150
|
+
cookiePairs = 0;
|
|
151
|
+
}
|
|
152
|
+
else if (word !== undefined && after.opensObject) {
|
|
153
|
+
mode = "object";
|
|
154
|
+
objectDepth = after.braceDepth;
|
|
155
|
+
objectEnd = end + OBJECT_SPAN;
|
|
156
|
+
}
|
|
157
|
+
else if (word !== undefined) {
|
|
158
|
+
mode = "scheme";
|
|
159
|
+
strict = STRICT_WORDS.has(word);
|
|
160
|
+
quote = after.quote;
|
|
161
|
+
quoteDepth = after.quoteBackslashes;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const previous = before;
|
|
165
|
+
before = after;
|
|
166
|
+
after = previous;
|
|
167
|
+
}
|
|
168
|
+
return ranges;
|
|
169
|
+
}
|
|
170
|
+
function replaceRanges(text, ranges, replacement) {
|
|
171
|
+
if (ranges.length === 0)
|
|
172
|
+
return text;
|
|
173
|
+
const out = [];
|
|
174
|
+
let copied = 0;
|
|
175
|
+
for (let k = 0; k < ranges.length; k += 2) {
|
|
176
|
+
out.push(text.slice(copied, ranges[k]), replacement);
|
|
177
|
+
copied = ranges[k + 1] ?? copied;
|
|
178
|
+
}
|
|
179
|
+
out.push(text.slice(copied));
|
|
180
|
+
return out.join("");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Mask credentials in provider or gateway text by value, not by a list of names: the value after a name that ends
|
|
184
|
+
* with a credential word (in a header, JSON, array, Go map, Python repr, XML, URL query, form body, cookie or
|
|
185
|
+
* environment assignment, with ASCII, typographic or HTML-escaped quotes, at any escaping depth; 16 characters or
|
|
186
|
+
* more, or the whole value at any length under a password, secret, authorization or bearer name), every cookie value,
|
|
187
|
+
* every long value of an object held under such a name, URL userinfo passwords, bearer tokens, and known key prefixes
|
|
188
|
+
* and JWTs wherever a word starts with one (inside URLs, userinfo and paths too). Model ids, error codes, request ids
|
|
189
|
+
* and names that merely contain a credential word stay readable. One linear pass, no regular expressions.
|
|
190
|
+
*/
|
|
191
|
+
export function redactSecrets(text, replacement = "[REDACTED]") {
|
|
192
|
+
return replaceRanges(text, secretRanges(text), replacement);
|
|
193
|
+
}
|
|
194
|
+
/** Mask only credential-shaped runs (known key prefixes and JWTs), whatever surrounds them. One linear pass. */
|
|
195
|
+
export function redactCredentialShapes(text, replacement = "[REDACTED]") {
|
|
196
|
+
return replaceRanges(text, secretRanges(text, true), replacement);
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=secret-redaction.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-ai",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.27-2",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@anthropic-ai/sdk": "0.127.0",
|
|
90
90
|
"@aws-sdk/client-bedrock-runtime": "3.1136.0",
|
|
91
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.
|
|
91
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.27-2",
|
|
92
92
|
"@google/genai": "2.23.0",
|
|
93
93
|
"@smithy/node-http-handler": "4.12.1",
|
|
94
94
|
"google-auth-library": "10.6.2",
|