te.js 2.1.5 → 2.2.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/auto-docs/analysis/handler-analyzer.test.js +106 -0
- package/auto-docs/analysis/source-resolver.test.js +58 -0
- package/auto-docs/constants.js +13 -2
- package/auto-docs/openapi/generator.js +7 -5
- package/auto-docs/openapi/generator.test.js +132 -0
- package/auto-docs/openapi/spec-builders.js +39 -19
- package/cli/docs-command.js +44 -36
- package/cors/index.test.js +82 -0
- package/database/index.js +3 -1
- package/database/mongodb.js +17 -11
- package/database/redis.js +53 -44
- package/docs/configuration.md +24 -10
- package/docs/error-handling.md +134 -50
- package/lib/llm/client.js +40 -10
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +281 -0
- package/rate-limit/index.js +8 -11
- package/rate-limit/index.test.js +64 -0
- package/server/ammo/body-parser.js +156 -152
- package/server/ammo/body-parser.test.js +79 -0
- package/server/ammo/enhancer.js +8 -4
- package/server/ammo.js +216 -17
- package/server/context/request-context.js +51 -0
- package/server/context/request-context.test.js +53 -0
- package/server/endpoint.js +15 -0
- package/server/error.js +56 -3
- package/server/error.test.js +45 -0
- package/server/errors/channels/base.js +31 -0
- package/server/errors/channels/channels.test.js +148 -0
- package/server/errors/channels/console.js +64 -0
- package/server/errors/channels/index.js +111 -0
- package/server/errors/channels/log.js +27 -0
- package/server/errors/llm-cache.js +102 -0
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +77 -16
- package/server/errors/llm-rate-limiter.js +72 -0
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +5 -3
- package/server/targets/registry.js +9 -9
- package/server/targets/registry.test.js +108 -0
- package/te.js +214 -57
- package/utils/auto-register.js +1 -1
- package/utils/configuration.js +23 -9
- package/utils/configuration.test.js +58 -0
- package/utils/errors-llm-config.js +142 -9
- package/utils/request-logger.js +49 -3
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { env } from 'tej-env';
|
|
8
|
+
import TejLogger from 'tej-logger';
|
|
9
|
+
|
|
10
|
+
const logger = new TejLogger('Tejas.ErrorsLlm');
|
|
8
11
|
|
|
9
12
|
const MESSAGE_TYPES = /** @type {const} */ (['endUser', 'developer']);
|
|
13
|
+
const LLM_MODES = /** @type {const} */ (['sync', 'async']);
|
|
14
|
+
const LLM_CHANNELS = /** @type {const} */ (['console', 'log', 'both']);
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Normalize messageType to 'endUser' | 'developer'.
|
|
@@ -14,14 +19,56 @@ const MESSAGE_TYPES = /** @type {const} */ (['endUser', 'developer']);
|
|
|
14
19
|
* @returns {'endUser'|'developer'}
|
|
15
20
|
*/
|
|
16
21
|
function normalizeMessageType(v) {
|
|
17
|
-
const s = String(v ?? '')
|
|
22
|
+
const s = String(v ?? '')
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase();
|
|
18
25
|
if (s === 'developer' || s === 'dev') return 'developer';
|
|
19
26
|
return 'endUser'; // endUser, end_user, default
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Normalize mode to 'sync' | 'async'.
|
|
31
|
+
* @param {string} v
|
|
32
|
+
* @returns {'sync'|'async'}
|
|
33
|
+
*/
|
|
34
|
+
function normalizeMode(v) {
|
|
35
|
+
const s = String(v ?? '')
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase();
|
|
38
|
+
if (s === 'async') return 'async';
|
|
39
|
+
return 'sync';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize channel to 'console' | 'log' | 'both'.
|
|
44
|
+
* @param {string} v
|
|
45
|
+
* @returns {'console'|'log'|'both'}
|
|
46
|
+
*/
|
|
47
|
+
function normalizeChannel(v) {
|
|
48
|
+
const s = String(v ?? '')
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
if (s === 'log') return 'log';
|
|
52
|
+
if (s === 'both') return 'both';
|
|
53
|
+
return 'console';
|
|
54
|
+
}
|
|
55
|
+
|
|
22
56
|
/**
|
|
23
57
|
* Resolve errors.llm config from env (feature-specific then LLM_ fallback).
|
|
24
|
-
* @returns {{
|
|
58
|
+
* @returns {{
|
|
59
|
+
* enabled: boolean,
|
|
60
|
+
* baseURL: string,
|
|
61
|
+
* apiKey: string,
|
|
62
|
+
* model: string,
|
|
63
|
+
* messageType: 'endUser'|'developer',
|
|
64
|
+
* mode: 'sync'|'async',
|
|
65
|
+
* timeout: number,
|
|
66
|
+
* channel: 'console'|'log'|'both',
|
|
67
|
+
* logFile: string,
|
|
68
|
+
* rateLimit: number,
|
|
69
|
+
* cache: boolean,
|
|
70
|
+
* cacheTTL: number,
|
|
71
|
+
* }}
|
|
25
72
|
*/
|
|
26
73
|
export function getErrorsLlmConfig() {
|
|
27
74
|
const enabledRaw = env('ERRORS_LLM_ENABLED') ?? '';
|
|
@@ -45,30 +92,86 @@ export function getErrorsLlmConfig() {
|
|
|
45
92
|
env('LLM_APIKEY') ??
|
|
46
93
|
'';
|
|
47
94
|
|
|
48
|
-
const model =
|
|
49
|
-
env('ERRORS_LLM_MODEL') ?? env('LLM_MODEL') ?? '';
|
|
95
|
+
const model = env('ERRORS_LLM_MODEL') ?? env('LLM_MODEL') ?? '';
|
|
50
96
|
|
|
51
97
|
const messageTypeRaw =
|
|
52
|
-
env('ERRORS_LLM_MESSAGE_TYPE') ??
|
|
98
|
+
env('ERRORS_LLM_MESSAGE_TYPE') ??
|
|
99
|
+
env('ERRORS_LLM_MESSAGETYPE') ??
|
|
100
|
+
env('LLM_MESSAGE_TYPE') ??
|
|
101
|
+
env('LLM_MESSAGETYPE') ??
|
|
102
|
+
'';
|
|
103
|
+
|
|
104
|
+
const modeRaw = env('ERRORS_LLM_MODE') ?? env('LLM_MODE') ?? '';
|
|
105
|
+
|
|
106
|
+
const timeoutRaw = env('ERRORS_LLM_TIMEOUT') ?? env('LLM_TIMEOUT') ?? '';
|
|
107
|
+
const timeoutNum = Number(timeoutRaw);
|
|
108
|
+
const timeout =
|
|
109
|
+
!timeoutRaw || isNaN(timeoutNum) || timeoutNum <= 0 ? 10000 : timeoutNum;
|
|
53
110
|
|
|
54
|
-
|
|
111
|
+
const channelRaw = env('ERRORS_LLM_CHANNEL') ?? env('LLM_CHANNEL') ?? '';
|
|
112
|
+
|
|
113
|
+
const logFile =
|
|
114
|
+
String(env('ERRORS_LLM_LOG_FILE') ?? '').trim() || './errors.llm.log';
|
|
115
|
+
|
|
116
|
+
const rateLimitRaw =
|
|
117
|
+
env('ERRORS_LLM_RATE_LIMIT') ?? env('LLM_RATE_LIMIT') ?? '';
|
|
118
|
+
const rateLimitNum = Number(rateLimitRaw);
|
|
119
|
+
const rateLimit =
|
|
120
|
+
!rateLimitRaw || isNaN(rateLimitNum) || rateLimitNum <= 0
|
|
121
|
+
? 10
|
|
122
|
+
: Math.floor(rateLimitNum);
|
|
123
|
+
|
|
124
|
+
const cacheRaw = env('ERRORS_LLM_CACHE') ?? '';
|
|
125
|
+
const cache =
|
|
126
|
+
cacheRaw === false ||
|
|
127
|
+
cacheRaw === 'false' ||
|
|
128
|
+
cacheRaw === '0' ||
|
|
129
|
+
cacheRaw === 0
|
|
130
|
+
? false
|
|
131
|
+
: true;
|
|
132
|
+
|
|
133
|
+
const cacheTTLRaw = env('ERRORS_LLM_CACHE_TTL') ?? '';
|
|
134
|
+
const cacheTTLNum = Number(cacheTTLRaw);
|
|
135
|
+
const cacheTTL =
|
|
136
|
+
!cacheTTLRaw || isNaN(cacheTTLNum) || cacheTTLNum <= 0
|
|
137
|
+
? 3600000
|
|
138
|
+
: cacheTTLNum;
|
|
139
|
+
|
|
140
|
+
return Object.freeze({
|
|
55
141
|
enabled: Boolean(enabled),
|
|
56
142
|
baseURL: String(baseURL ?? '').trim(),
|
|
57
143
|
apiKey: String(apiKey ?? '').trim(),
|
|
58
144
|
model: String(model ?? '').trim(),
|
|
59
145
|
messageType: normalizeMessageType(messageTypeRaw || 'endUser'),
|
|
60
|
-
|
|
146
|
+
mode: normalizeMode(modeRaw),
|
|
147
|
+
timeout,
|
|
148
|
+
channel: normalizeChannel(channelRaw),
|
|
149
|
+
logFile,
|
|
150
|
+
rateLimit,
|
|
151
|
+
cache,
|
|
152
|
+
cacheTTL,
|
|
153
|
+
});
|
|
61
154
|
}
|
|
62
155
|
|
|
63
|
-
export { MESSAGE_TYPES };
|
|
156
|
+
export { MESSAGE_TYPES, LLM_MODES, LLM_CHANNELS };
|
|
64
157
|
|
|
65
158
|
/**
|
|
66
159
|
* Validate errors.llm when enabled: require baseURL, apiKey, and model (after LLM_ fallback).
|
|
160
|
+
* Also warns about misconfigurations (e.g. channel set with sync mode).
|
|
67
161
|
* Call at takeoff. Throws if enabled but config is invalid; no-op otherwise.
|
|
68
162
|
* @throws {Error} If errors.llm.enabled is true but any of baseURL, apiKey, or model is missing
|
|
69
163
|
*/
|
|
70
164
|
export function validateErrorsLlmAtTakeoff() {
|
|
71
|
-
const {
|
|
165
|
+
const {
|
|
166
|
+
enabled,
|
|
167
|
+
baseURL,
|
|
168
|
+
apiKey,
|
|
169
|
+
model,
|
|
170
|
+
mode,
|
|
171
|
+
channel,
|
|
172
|
+
rateLimit,
|
|
173
|
+
cacheTTL,
|
|
174
|
+
} = getErrorsLlmConfig();
|
|
72
175
|
if (!enabled) return;
|
|
73
176
|
|
|
74
177
|
const missing = [];
|
|
@@ -81,4 +184,34 @@ export function validateErrorsLlmAtTakeoff() {
|
|
|
81
184
|
`errors.llm is enabled but required config is missing: ${missing.join(', ')}. Set these env vars or disable errors.llm.enabled.`,
|
|
82
185
|
);
|
|
83
186
|
}
|
|
187
|
+
|
|
188
|
+
// Warn about channel set while mode is sync (channel only applies in async mode).
|
|
189
|
+
const channelRaw = String(
|
|
190
|
+
env('ERRORS_LLM_CHANNEL') ?? env('LLM_CHANNEL') ?? '',
|
|
191
|
+
).trim();
|
|
192
|
+
if (mode === 'sync' && channelRaw) {
|
|
193
|
+
logger.warn(
|
|
194
|
+
`errors.llm: channel="${channel}" is set but mode is "sync" — channel output only applies in async mode. Set ERRORS_LLM_MODE=async to use it.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Warn about invalid numeric values that were silently reset to defaults.
|
|
199
|
+
const rateLimitRaw = String(
|
|
200
|
+
env('ERRORS_LLM_RATE_LIMIT') ?? env('LLM_RATE_LIMIT') ?? '',
|
|
201
|
+
).trim();
|
|
202
|
+
if (
|
|
203
|
+
rateLimitRaw &&
|
|
204
|
+
(isNaN(Number(rateLimitRaw)) || Number(rateLimitRaw) <= 0)
|
|
205
|
+
) {
|
|
206
|
+
logger.warn(
|
|
207
|
+
`errors.llm: rateLimit value "${rateLimitRaw}" is invalid; defaulting to 10.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cacheTTLRaw = String(env('ERRORS_LLM_CACHE_TTL') ?? '').trim();
|
|
212
|
+
if (cacheTTLRaw && (isNaN(Number(cacheTTLRaw)) || Number(cacheTTLRaw) <= 0)) {
|
|
213
|
+
logger.warn(
|
|
214
|
+
`errors.llm: cacheTTL value "${cacheTTLRaw}" is invalid; defaulting to 3600000.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
84
217
|
}
|
package/utils/request-logger.js
CHANGED
|
@@ -5,11 +5,46 @@ import TejLogger from 'tej-logger';
|
|
|
5
5
|
const logger = new TejLogger('Tejas.Request');
|
|
6
6
|
const { italic, bold, blue, white, bgGreen, bgRed, whiteBright } = ansi;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Best-effort field names to mask when logging request/response bodies to the
|
|
10
|
+
* console. This is a hardcoded safety net — it does not replace the
|
|
11
|
+
* non-bypassable scrubbing enforced by the Radar collector on telemetry data.
|
|
12
|
+
*/
|
|
13
|
+
const CONSOLE_MASK_FIELDS = new Set([
|
|
14
|
+
'password',
|
|
15
|
+
'passwd',
|
|
16
|
+
'secret',
|
|
17
|
+
'token',
|
|
18
|
+
'authorization',
|
|
19
|
+
'api_key',
|
|
20
|
+
'apikey',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively mask sensitive fields in a value for safe console output.
|
|
25
|
+
* Replaces matched key values with `"*"`.
|
|
26
|
+
*
|
|
27
|
+
* @param {unknown} value
|
|
28
|
+
* @returns {unknown}
|
|
29
|
+
*/
|
|
30
|
+
function maskForLog(value) {
|
|
31
|
+
if (value === null || typeof value !== 'object') return value;
|
|
32
|
+
if (Array.isArray(value)) return value.map(maskForLog);
|
|
33
|
+
|
|
34
|
+
const result = Object.create(null);
|
|
35
|
+
for (const [k, v] of Object.entries(value)) {
|
|
36
|
+
result[k] = CONSOLE_MASK_FIELDS.has(k.toLowerCase()) ? '*' : maskForLog(v);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
function logHttpRequest(ammo, next) {
|
|
9
42
|
if (!env('LOG_HTTP_REQUESTS')) return;
|
|
10
43
|
|
|
11
44
|
const startTime = new Date();
|
|
12
|
-
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
ammo.res.on('finish', { signal: controller.signal }, () => {
|
|
47
|
+
controller.abort();
|
|
13
48
|
const res = ammo.res;
|
|
14
49
|
const method = italic(whiteBright(ammo.method));
|
|
15
50
|
const endpoint = bold(ammo.endpoint);
|
|
@@ -19,10 +54,21 @@ function logHttpRequest(ammo, next) {
|
|
|
19
54
|
: bgGreen(whiteBright(bold(`✔ ${res.statusCode}`)));
|
|
20
55
|
|
|
21
56
|
const duration = white(`${new Date() - startTime}ms`);
|
|
57
|
+
|
|
58
|
+
const maskedPayload = maskForLog(ammo.payload);
|
|
22
59
|
const payload = `${blue('Request')}: ${white(
|
|
23
|
-
JSON.stringify(
|
|
60
|
+
JSON.stringify(maskedPayload),
|
|
24
61
|
)}`;
|
|
25
|
-
|
|
62
|
+
|
|
63
|
+
let maskedResponse = ammo.dispatchedData;
|
|
64
|
+
try {
|
|
65
|
+
maskedResponse = JSON.stringify(
|
|
66
|
+
maskForLog(JSON.parse(ammo.dispatchedData)),
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
// Non-JSON response — log as-is
|
|
70
|
+
}
|
|
71
|
+
const dispatchedData = `${blue('Response')}: ${white(maskedResponse)}`;
|
|
26
72
|
const nextLine = '\n';
|
|
27
73
|
|
|
28
74
|
logger.log(
|