te.js 2.1.6 → 2.2.1

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.
Files changed (55) hide show
  1. package/README.md +1 -12
  2. package/auto-docs/analysis/handler-analyzer.test.js +106 -0
  3. package/auto-docs/analysis/source-resolver.test.js +58 -0
  4. package/auto-docs/constants.js +13 -2
  5. package/auto-docs/openapi/generator.js +7 -5
  6. package/auto-docs/openapi/generator.test.js +132 -0
  7. package/auto-docs/openapi/spec-builders.js +39 -19
  8. package/cli/docs-command.js +44 -36
  9. package/cors/index.test.js +82 -0
  10. package/docs/README.md +1 -2
  11. package/docs/api-reference.md +124 -186
  12. package/docs/configuration.md +0 -13
  13. package/docs/getting-started.md +19 -21
  14. package/docs/rate-limiting.md +59 -58
  15. package/lib/llm/client.js +7 -2
  16. package/lib/llm/index.js +14 -1
  17. package/lib/llm/parse.test.js +60 -0
  18. package/package.json +3 -1
  19. package/radar/index.js +382 -0
  20. package/rate-limit/base.js +12 -15
  21. package/rate-limit/index.js +19 -22
  22. package/rate-limit/index.test.js +93 -0
  23. package/rate-limit/storage/memory.js +13 -13
  24. package/rate-limit/storage/redis-install.js +70 -0
  25. package/rate-limit/storage/redis.js +94 -52
  26. package/server/ammo/body-parser.js +156 -152
  27. package/server/ammo/body-parser.test.js +79 -0
  28. package/server/ammo/enhancer.js +8 -4
  29. package/server/ammo.js +138 -12
  30. package/server/context/request-context.js +51 -0
  31. package/server/context/request-context.test.js +53 -0
  32. package/server/endpoint.js +15 -0
  33. package/server/error.js +56 -3
  34. package/server/error.test.js +45 -0
  35. package/server/errors/channels/channels.test.js +148 -0
  36. package/server/errors/channels/index.js +1 -1
  37. package/server/errors/llm-cache.js +1 -1
  38. package/server/errors/llm-cache.test.js +160 -0
  39. package/server/errors/llm-error-service.js +1 -1
  40. package/server/errors/llm-rate-limiter.test.js +105 -0
  41. package/server/files/uploader.js +38 -26
  42. package/server/handler.js +1 -1
  43. package/server/targets/registry.js +3 -3
  44. package/server/targets/registry.test.js +108 -0
  45. package/te.js +233 -183
  46. package/utils/auto-register.js +1 -1
  47. package/utils/configuration.js +23 -9
  48. package/utils/configuration.test.js +58 -0
  49. package/utils/errors-llm-config.js +74 -8
  50. package/utils/request-logger.js +49 -3
  51. package/utils/startup.js +80 -0
  52. package/database/index.js +0 -165
  53. package/database/mongodb.js +0 -146
  54. package/database/redis.js +0 -201
  55. package/docs/database.md +0 -390
@@ -5,6 +5,9 @@
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']);
10
13
  const LLM_MODES = /** @type {const} */ (['sync', 'async']);
@@ -65,6 +68,7 @@ function normalizeChannel(v) {
65
68
  * rateLimit: number,
66
69
  * cache: boolean,
67
70
  * cacheTTL: number,
71
+ * verifyOnStart: boolean,
68
72
  * }}
69
73
  */
70
74
  export function getErrorsLlmConfig() {
@@ -134,7 +138,14 @@ export function getErrorsLlmConfig() {
134
138
  ? 3600000
135
139
  : cacheTTLNum;
136
140
 
137
- return {
141
+ const verifyOnStartRaw = env('ERRORS_LLM_VERIFY_ON_START') ?? '';
142
+ const verifyOnStart =
143
+ verifyOnStartRaw === true ||
144
+ verifyOnStartRaw === 'true' ||
145
+ verifyOnStartRaw === '1' ||
146
+ verifyOnStartRaw === 1;
147
+
148
+ return Object.freeze({
138
149
  enabled: Boolean(enabled),
139
150
  baseURL: String(baseURL ?? '').trim(),
140
151
  apiKey: String(apiKey ?? '').trim(),
@@ -147,11 +158,66 @@ export function getErrorsLlmConfig() {
147
158
  rateLimit,
148
159
  cache,
149
160
  cacheTTL,
150
- };
161
+ verifyOnStart,
162
+ });
151
163
  }
152
164
 
153
165
  export { MESSAGE_TYPES, LLM_MODES, LLM_CHANNELS };
154
166
 
167
+ /**
168
+ * Fire a lightweight probe to the configured LLM provider and verify it
169
+ * responds correctly. Intended to run once at takeoff when `verifyOnStart: true`.
170
+ *
171
+ * Never throws — a flaky provider does not prevent the server from starting.
172
+ *
173
+ * @returns {Promise<{ ok: boolean, status: { feature: string, ok: boolean, detail: string } }>}
174
+ */
175
+ export async function verifyLlmConnection() {
176
+ const { baseURL, apiKey, model, timeout, mode } = getErrorsLlmConfig();
177
+
178
+ const { createProvider } = await import('../lib/llm/index.js');
179
+ const provider = createProvider({ baseURL, apiKey, model, timeout });
180
+
181
+ const shortModel = model.split('/').pop().split(':')[0] || model;
182
+ const start = Date.now();
183
+ try {
184
+ const { content } = await provider.analyze(
185
+ 'Respond with only the JSON object {"status":"ok"}. No explanation.',
186
+ );
187
+ const elapsed = Date.now() - start;
188
+
189
+ if (content.includes('"ok"')) {
190
+ return {
191
+ ok: true,
192
+ status: {
193
+ feature: 'LLM Errors',
194
+ ok: true,
195
+ detail: `verified (${shortModel}, ${elapsed}ms, mode: ${mode})`,
196
+ },
197
+ };
198
+ }
199
+
200
+ return {
201
+ ok: false,
202
+ status: {
203
+ feature: 'LLM Errors',
204
+ ok: false,
205
+ detail: `unexpected response from ${shortModel} (${elapsed}ms)`,
206
+ },
207
+ };
208
+ } catch (err) {
209
+ const elapsed = Date.now() - start;
210
+ return {
211
+ ok: false,
212
+ status: {
213
+ feature: 'LLM Errors',
214
+ ok: false,
215
+ detail: `${err.message} (${elapsed}ms)`,
216
+ },
217
+ };
218
+ }
219
+ }
220
+
155
221
  /**
156
222
  * Validate errors.llm when enabled: require baseURL, apiKey, and model (after LLM_ fallback).
157
223
  * Also warns about misconfigurations (e.g. channel set with sync mode).
@@ -187,8 +253,8 @@ export function validateErrorsLlmAtTakeoff() {
187
253
  env('ERRORS_LLM_CHANNEL') ?? env('LLM_CHANNEL') ?? '',
188
254
  ).trim();
189
255
  if (mode === 'sync' && channelRaw) {
190
- console.warn(
191
- `[Tejas] 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.`,
256
+ logger.warn(
257
+ `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.`,
192
258
  );
193
259
  }
194
260
 
@@ -200,15 +266,15 @@ export function validateErrorsLlmAtTakeoff() {
200
266
  rateLimitRaw &&
201
267
  (isNaN(Number(rateLimitRaw)) || Number(rateLimitRaw) <= 0)
202
268
  ) {
203
- console.warn(
204
- `[Tejas] errors.llm: rateLimit value "${rateLimitRaw}" is invalid; defaulting to 10.`,
269
+ logger.warn(
270
+ `errors.llm: rateLimit value "${rateLimitRaw}" is invalid; defaulting to 10.`,
205
271
  );
206
272
  }
207
273
 
208
274
  const cacheTTLRaw = String(env('ERRORS_LLM_CACHE_TTL') ?? '').trim();
209
275
  if (cacheTTLRaw && (isNaN(Number(cacheTTLRaw)) || Number(cacheTTLRaw) <= 0)) {
210
- console.warn(
211
- `[Tejas] errors.llm: cacheTTL value "${cacheTTLRaw}" is invalid; defaulting to 3600000.`,
276
+ logger.warn(
277
+ `errors.llm: cacheTTL value "${cacheTTLRaw}" is invalid; defaulting to 3600000.`,
212
278
  );
213
279
  }
214
280
  }
@@ -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
- ammo.res.on('finish', () => {
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(ammo.payload),
60
+ JSON.stringify(maskedPayload),
24
61
  )}`;
25
- const dispatchedData = `${blue('Response')}: ${white(ammo.dispatchedData)}`;
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(
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ /**
6
+ * Read the framework's own package.json version.
7
+ * Resolves relative to the framework source, not the user's app.
8
+ * @returns {Promise<string>}
9
+ */
10
+ export async function readFrameworkVersion() {
11
+ try {
12
+ const dir = path.dirname(fileURLToPath(import.meta.url));
13
+ const raw = await readFile(path.join(dir, '..', 'package.json'), 'utf8');
14
+ return JSON.parse(raw).version ?? 'unknown';
15
+ } catch {
16
+ return 'unknown';
17
+ }
18
+ }
19
+
20
+ /** Format milliseconds into a compact human-readable string. */
21
+ export function fmtMs(ms) {
22
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
23
+ }
24
+
25
+ const SPINNER = [
26
+ '\u280B',
27
+ '\u2819',
28
+ '\u2839',
29
+ '\u2838',
30
+ '\u283C',
31
+ '\u2834',
32
+ '\u2826',
33
+ '\u2827',
34
+ '\u2807',
35
+ '\u280F',
36
+ ];
37
+
38
+ /**
39
+ * Create a live status line writer for startup progress.
40
+ * On TTY terminals, shows a spinning animation that gets overwritten in-place
41
+ * when the step completes. On non-TTY (piped, CI), only prints the final result.
42
+ * @param {boolean} isTTY
43
+ */
44
+ export function statusLine(isTTY) {
45
+ let timer = null;
46
+
47
+ function stop() {
48
+ if (timer) {
49
+ clearInterval(timer);
50
+ timer = null;
51
+ }
52
+ }
53
+
54
+ return {
55
+ start(feature, message) {
56
+ if (!isTTY) return;
57
+ let frame = 0;
58
+ const render = () => {
59
+ const s = SPINNER[frame % SPINNER.length];
60
+ process.stdout.write(
61
+ `\r\x1b[K ${feature.padEnd(14)} \x1b[36m${s}\x1b[0m \x1b[2m${message}\x1b[0m`,
62
+ );
63
+ frame++;
64
+ };
65
+ render();
66
+ timer = setInterval(render, 80);
67
+ },
68
+ finish(feature, ok, detail) {
69
+ stop();
70
+ if (isTTY) process.stdout.write('\r\x1b[K');
71
+ const icon =
72
+ ok === true
73
+ ? '\x1b[32m\u2713\x1b[0m'
74
+ : ok === false
75
+ ? '\x1b[31m\u2717\x1b[0m'
76
+ : '\x1b[2m\u2014\x1b[0m';
77
+ process.stdout.write(` ${feature.padEnd(14)} ${icon} ${detail}\n`);
78
+ },
79
+ };
80
+ }
package/database/index.js DELETED
@@ -1,165 +0,0 @@
1
- import redis from './redis.js';
2
- import mongodb from './mongodb.js';
3
- import TejError from '../server/error.js';
4
- import TejLogger from 'tej-logger';
5
-
6
- const logger = new TejLogger('DatabaseManager');
7
-
8
- class DatabaseManager {
9
- static #instance = null;
10
- static #isInitializing = false;
11
-
12
- // Enhanced connection tracking with metadata
13
- #connections = new Map();
14
- #initializingConnections = new Map();
15
-
16
- // Helper method for sleeping
17
- async #sleep(ms) {
18
- return new Promise((resolve) => setTimeout(resolve, ms));
19
- }
20
-
21
- constructor() {
22
- if (DatabaseManager.#instance) {
23
- return DatabaseManager.#instance;
24
- }
25
-
26
- if (!DatabaseManager.#isInitializing) {
27
- throw new TejError(
28
- 500,
29
- 'Use DatabaseManager.getInstance() to get the instance',
30
- );
31
- }
32
-
33
- DatabaseManager.#isInitializing = false;
34
- DatabaseManager.#instance = this;
35
- }
36
-
37
- static getInstance() {
38
- if (!DatabaseManager.#instance) {
39
- DatabaseManager.#isInitializing = true;
40
- DatabaseManager.#instance = new DatabaseManager();
41
- }
42
- return DatabaseManager.#instance;
43
- }
44
-
45
- async initializeConnection(dbType, config) {
46
- const key = dbType.toLowerCase();
47
-
48
- // If a connection already exists for this config, return it
49
- if (this.#connections.has(key)) {
50
- return this.#connections.get(key).client;
51
- }
52
-
53
- // Set initializing flag
54
- this.#initializingConnections.set(key, true);
55
-
56
- let client;
57
- try {
58
- switch (key) {
59
- case 'redis':
60
- client = await redis.createConnection({
61
- isCluster: config.isCluster || false,
62
- options: config || {},
63
- });
64
- break;
65
- case 'mongodb':
66
- client = await mongodb.createConnection(config);
67
- break;
68
- default:
69
- throw new TejError(400, `Unsupported database type: ${dbType}`);
70
- }
71
-
72
- this.#connections.set(key, {
73
- type: dbType,
74
- client,
75
- config,
76
- });
77
-
78
- // Clear initializing flag
79
- this.#initializingConnections.delete(key);
80
-
81
- return client;
82
- } catch (error) {
83
- // Clear initializing flag on error
84
- this.#initializingConnections.delete(key);
85
- logger.error(`Failed to initialize ${dbType} connection:`, error);
86
- throw error;
87
- }
88
- }
89
-
90
- getConnection(dbType) {
91
- const key = dbType.toLowerCase();
92
- const connection = this.#connections.get(key);
93
- if (!connection) {
94
- throw new TejError(
95
- 404,
96
- `No connection found for ${dbType} with given config`,
97
- );
98
- }
99
- return connection.client;
100
- }
101
-
102
- async closeConnection(dbType, config) {
103
- const key = dbType.toLowerCase();
104
- if (!this.#connections.has(key)) {
105
- return;
106
- }
107
-
108
- try {
109
- const connection = this.#connections.get(key);
110
- switch (key) {
111
- case 'redis':
112
- await redis.closeConnection(connection.client);
113
- break;
114
- case 'mongodb':
115
- await mongodb.closeConnection(connection.client);
116
- break;
117
- }
118
-
119
- this.#connections.delete(key);
120
- } catch (error) {
121
- logger.error(`Error closing ${dbType} connection:`, error);
122
- throw error;
123
- }
124
- }
125
-
126
- /**
127
- * Close all database connections
128
- * @returns {Promise<void>}
129
- */
130
- async closeAllConnections() {
131
- const closePromises = [];
132
- for (const [key, connection] of this.#connections) {
133
- closePromises.push(
134
- this.closeConnection(connection.type, connection.config),
135
- );
136
- }
137
- await Promise.all(closePromises);
138
- this.#connections.clear();
139
- }
140
-
141
- /**
142
- * Get all active connections
143
- * @returns {Map<string, {type: string, client: any, config: Object}>}
144
- */
145
- getActiveConnections() {
146
- return new Map(this.#connections);
147
- }
148
-
149
- /**
150
- * Check if a connection exists or is being initialized
151
- * @param {string} dbType - Type of database
152
- * @param {Object} config - Database configuration
153
- * @returns {{ exists: boolean, initializing: boolean }}
154
- */
155
- hasConnection(dbType, config) {
156
- const key = dbType.toLowerCase();
157
- return {
158
- exists: this.#connections.has(key),
159
- initializing: this.#initializingConnections.has(key),
160
- };
161
- }
162
- }
163
-
164
- const dbManager = DatabaseManager.getInstance();
165
- export default dbManager;
@@ -1,146 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import TejLogger from 'tej-logger';
6
- import TejError from '../server/error.js';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- const logger = new TejLogger('MongoDBConnectionManager');
12
-
13
- function checkMongooseInstallation() {
14
- const packageJsonPath = path.join(__dirname, '..', 'package.json');
15
- const nodeModulesPath = path.join(
16
- __dirname,
17
- '..',
18
- 'node_modules',
19
- 'mongoose',
20
- );
21
-
22
- try {
23
- // Check if mongoose exists in package.json
24
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
25
- const inPackageJson = !!packageJson.dependencies?.mongoose;
26
-
27
- // Check if mongoose exists in node_modules
28
- const inNodeModules = fs.existsSync(nodeModulesPath);
29
-
30
- return {
31
- needsInstall: !inPackageJson || !inNodeModules,
32
- reason: !inPackageJson
33
- ? 'not in package.json'
34
- : !inNodeModules
35
- ? 'not in node_modules'
36
- : null,
37
- };
38
- } catch (error) {
39
- return { needsInstall: true, reason: 'error checking installation' };
40
- }
41
- }
42
-
43
- function installMongooseSync() {
44
- const spinner = ['|', '/', '-', '\\'];
45
- let current = 0;
46
- let intervalId;
47
-
48
- try {
49
- const { needsInstall, reason } = checkMongooseInstallation();
50
-
51
- if (!needsInstall) {
52
- return true;
53
- }
54
-
55
- // Start the spinner
56
- intervalId = setInterval(() => {
57
- process.stdout.write(`\r${spinner[current]} Installing mongoose...`);
58
- current = (current + 1) % spinner.length;
59
- }, 100);
60
-
61
- logger.info(`Tejas will install mongoose (${reason})...`);
62
-
63
- const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
64
- const result = spawn.sync(command, ['install', 'mongoose'], {
65
- stdio: 'inherit',
66
- shell: true,
67
- });
68
-
69
- process.stdout.write('\r');
70
- clearInterval(intervalId);
71
-
72
- if (result.status === 0) {
73
- logger.info('Mongoose installed successfully');
74
- return true;
75
- } else {
76
- logger.error('Mongoose installation failed');
77
- return false;
78
- }
79
- } catch (error) {
80
- if (intervalId) {
81
- process.stdout.write('\r');
82
- clearInterval(intervalId);
83
- }
84
- logger.error('Error installing mongoose:', error);
85
- return false;
86
- }
87
- }
88
-
89
- /**
90
- * Create a new MongoDB connection
91
- * @param {Object} config - MongoDB configuration
92
- * @param {string} config.uri - MongoDB connection URI
93
- * @param {Object} [config.options={}] - Additional Mongoose options
94
- * @returns {Promise<mongoose.Connection>} Mongoose connection instance
95
- */
96
- async function createConnection(config) {
97
- const { needsInstall } = checkMongooseInstallation();
98
-
99
- if (needsInstall) {
100
- const installed = installMongooseSync();
101
- if (!installed) {
102
- throw new TejError(500, 'Failed to install required mongoose package');
103
- }
104
- }
105
-
106
- const { uri, options = {} } = config;
107
-
108
- try {
109
- const mongoose = await import('mongoose').then((mod) => mod.default);
110
- const connection = await mongoose.createConnection(uri, options);
111
-
112
- connection.on('error', (err) =>
113
- logger.error(`MongoDB connection error:`, err),
114
- );
115
- connection.on('connected', () => {
116
- logger.info(`MongoDB connected to ${uri}`);
117
- });
118
- connection.on('disconnected', () => {
119
- logger.info(`MongoDB disconnected from ${uri}`);
120
- });
121
-
122
- return connection;
123
- } catch (error) {
124
- logger.error(`Failed to create MongoDB connection:`, error);
125
- throw new TejError(
126
- 500,
127
- `Failed to create MongoDB connection: ${error.message}`,
128
- );
129
- }
130
- }
131
-
132
- /**
133
- * Close a MongoDB connection
134
- * @param {mongoose.Connection} connection - Mongoose connection to close
135
- * @returns {Promise<void>}
136
- */
137
- async function closeConnection(connection) {
138
- if (connection) {
139
- await connection.close();
140
- }
141
- }
142
-
143
- export default {
144
- createConnection,
145
- closeConnection,
146
- };