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.
Files changed (49) hide show
  1. package/auto-docs/analysis/handler-analyzer.test.js +106 -0
  2. package/auto-docs/analysis/source-resolver.test.js +58 -0
  3. package/auto-docs/constants.js +13 -2
  4. package/auto-docs/openapi/generator.js +7 -5
  5. package/auto-docs/openapi/generator.test.js +132 -0
  6. package/auto-docs/openapi/spec-builders.js +39 -19
  7. package/cli/docs-command.js +44 -36
  8. package/cors/index.test.js +82 -0
  9. package/database/index.js +3 -1
  10. package/database/mongodb.js +17 -11
  11. package/database/redis.js +53 -44
  12. package/docs/configuration.md +24 -10
  13. package/docs/error-handling.md +134 -50
  14. package/lib/llm/client.js +40 -10
  15. package/lib/llm/index.js +14 -1
  16. package/lib/llm/parse.test.js +60 -0
  17. package/package.json +3 -1
  18. package/radar/index.js +281 -0
  19. package/rate-limit/index.js +8 -11
  20. package/rate-limit/index.test.js +64 -0
  21. package/server/ammo/body-parser.js +156 -152
  22. package/server/ammo/body-parser.test.js +79 -0
  23. package/server/ammo/enhancer.js +8 -4
  24. package/server/ammo.js +216 -17
  25. package/server/context/request-context.js +51 -0
  26. package/server/context/request-context.test.js +53 -0
  27. package/server/endpoint.js +15 -0
  28. package/server/error.js +56 -3
  29. package/server/error.test.js +45 -0
  30. package/server/errors/channels/base.js +31 -0
  31. package/server/errors/channels/channels.test.js +148 -0
  32. package/server/errors/channels/console.js +64 -0
  33. package/server/errors/channels/index.js +111 -0
  34. package/server/errors/channels/log.js +27 -0
  35. package/server/errors/llm-cache.js +102 -0
  36. package/server/errors/llm-cache.test.js +160 -0
  37. package/server/errors/llm-error-service.js +77 -16
  38. package/server/errors/llm-rate-limiter.js +72 -0
  39. package/server/errors/llm-rate-limiter.test.js +105 -0
  40. package/server/files/uploader.js +38 -26
  41. package/server/handler.js +5 -3
  42. package/server/targets/registry.js +9 -9
  43. package/server/targets/registry.test.js +108 -0
  44. package/te.js +214 -57
  45. package/utils/auto-register.js +1 -1
  46. package/utils/configuration.js +23 -9
  47. package/utils/configuration.test.js +58 -0
  48. package/utils/errors-llm-config.js +142 -9
  49. package/utils/request-logger.js +49 -3
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview Tests for CORS middleware.
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import corsMiddleware from './index.js';
6
+
7
+ function makeAmmo(method = 'GET', origin = 'https://example.com') {
8
+ const headers = {};
9
+ return {
10
+ req: { method, headers: { origin } },
11
+ res: {
12
+ _headers: {},
13
+ setHeader(name, value) {
14
+ this._headers[name.toLowerCase()] = value;
15
+ },
16
+ writeHead(code) {
17
+ this._status = code;
18
+ },
19
+ end() {
20
+ this._ended = true;
21
+ },
22
+ },
23
+ };
24
+ }
25
+
26
+ describe('corsMiddleware', () => {
27
+ it('should set Access-Control-Allow-Origin to * by default', async () => {
28
+ const mw = corsMiddleware();
29
+ const ammo = makeAmmo();
30
+ const next = vi.fn();
31
+ await mw(ammo, next);
32
+ expect(ammo.res._headers['access-control-allow-origin']).toBe('*');
33
+ expect(next).toHaveBeenCalled();
34
+ });
35
+
36
+ it('should respond 204 and not call next for OPTIONS preflight', async () => {
37
+ const mw = corsMiddleware();
38
+ const ammo = makeAmmo('OPTIONS');
39
+ const next = vi.fn();
40
+ await mw(ammo, next);
41
+ expect(ammo.res._status).toBe(204);
42
+ expect(ammo.res._ended).toBe(true);
43
+ expect(next).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it('should allow specific origin from array', async () => {
47
+ const mw = corsMiddleware({ origin: ['https://example.com'] });
48
+ const ammo = makeAmmo('GET', 'https://example.com');
49
+ const next = vi.fn();
50
+ await mw(ammo, next);
51
+ expect(ammo.res._headers['access-control-allow-origin']).toBe(
52
+ 'https://example.com',
53
+ );
54
+ });
55
+
56
+ it('should block origins not in array', async () => {
57
+ const mw = corsMiddleware({ origin: ['https://example.com'] });
58
+ const ammo = makeAmmo('GET', 'https://evil.com');
59
+ const next = vi.fn();
60
+ await mw(ammo, next);
61
+ expect(ammo.res._headers['access-control-allow-origin']).toBeUndefined();
62
+ });
63
+
64
+ it('should set Access-Control-Max-Age when maxAge provided', async () => {
65
+ const mw = corsMiddleware({ maxAge: 86400 });
66
+ const ammo = makeAmmo();
67
+ const next = vi.fn();
68
+ await mw(ammo, next);
69
+ expect(ammo.res._headers['access-control-max-age']).toBe('86400');
70
+ });
71
+
72
+ it('should set Access-Control-Allow-Credentials when credentials=true', async () => {
73
+ const mw = corsMiddleware({
74
+ credentials: true,
75
+ origin: 'https://example.com',
76
+ });
77
+ const ammo = makeAmmo('GET', 'https://example.com');
78
+ const next = vi.fn();
79
+ await mw(ammo, next);
80
+ expect(ammo.res._headers['access-control-allow-credentials']).toBe('true');
81
+ });
82
+ });
package/database/index.js CHANGED
@@ -15,7 +15,9 @@ class DatabaseManager {
15
15
 
16
16
  // Helper method for sleeping
17
17
  async #sleep(ms) {
18
- return new Promise((resolve) => setTimeout(resolve, ms));
18
+ const { promise, resolve } = Promise.withResolvers();
19
+ setTimeout(resolve, ms);
20
+ return promise;
19
21
  }
20
22
 
21
23
  constructor() {
@@ -1,7 +1,7 @@
1
- import { spawn } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
5
  import TejLogger from 'tej-logger';
6
6
  import TejError from '../server/error.js';
7
7
 
@@ -10,7 +10,7 @@ const __dirname = path.dirname(__filename);
10
10
 
11
11
  const logger = new TejLogger('MongoDBConnectionManager');
12
12
 
13
- function checkMongooseInstallation() {
13
+ async function checkMongooseInstallation() {
14
14
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
15
15
  const nodeModulesPath = path.join(
16
16
  __dirname,
@@ -20,12 +20,18 @@ function checkMongooseInstallation() {
20
20
  );
21
21
 
22
22
  try {
23
- // Check if mongoose exists in package.json
24
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
23
+ const packageJson = JSON.parse(
24
+ await fs.promises.readFile(packageJsonPath, 'utf8'),
25
+ );
25
26
  const inPackageJson = !!packageJson.dependencies?.mongoose;
26
27
 
27
- // Check if mongoose exists in node_modules
28
- const inNodeModules = fs.existsSync(nodeModulesPath);
28
+ let inNodeModules = false;
29
+ try {
30
+ await fs.promises.access(nodeModulesPath);
31
+ inNodeModules = true;
32
+ } catch {
33
+ inNodeModules = false;
34
+ }
29
35
 
30
36
  return {
31
37
  needsInstall: !inPackageJson || !inNodeModules,
@@ -94,7 +100,7 @@ function installMongooseSync() {
94
100
  * @returns {Promise<mongoose.Connection>} Mongoose connection instance
95
101
  */
96
102
  async function createConnection(config) {
97
- const { needsInstall } = checkMongooseInstallation();
103
+ const { needsInstall } = await checkMongooseInstallation();
98
104
 
99
105
  if (needsInstall) {
100
106
  const installed = installMongooseSync();
@@ -106,7 +112,7 @@ async function createConnection(config) {
106
112
  const { uri, options = {} } = config;
107
113
 
108
114
  try {
109
- const mongoose = await import('mongoose').then((mod) => mod.default);
115
+ const { default: mongoose } = await import('mongoose');
110
116
  const connection = await mongoose.createConnection(uri, options);
111
117
 
112
118
  connection.on('error', (err) =>
package/database/redis.js CHANGED
@@ -1,6 +1,6 @@
1
- import { spawnSync } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
4
  import TejError from '../server/error.js';
5
5
  import TejLogger from 'tej-logger';
6
6
  import { pathToFileURL } from 'node:url';
@@ -10,14 +10,20 @@ const packagePath = `${process.cwd()}/node_modules/redis/dist/index.js`;
10
10
 
11
11
  const logger = new TejLogger('RedisConnectionManager');
12
12
 
13
- function checkRedisInstallation() {
13
+ async function checkRedisInstallation() {
14
14
  try {
15
- // Check if redis exists in package.json
16
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
15
+ const packageJson = JSON.parse(
16
+ await fs.promises.readFile(packageJsonPath, 'utf8'),
17
+ );
17
18
  const inPackageJson = !!packageJson.dependencies?.redis;
18
19
 
19
- // Check if redis exists in node_modules
20
- const inNodeModules = fs.existsSync(packagePath);
20
+ let inNodeModules = false;
21
+ try {
22
+ await fs.promises.access(packagePath);
23
+ inNodeModules = true;
24
+ } catch {
25
+ inNodeModules = false;
26
+ }
21
27
 
22
28
  return {
23
29
  needsInstall: !inPackageJson || !inNodeModules,
@@ -87,7 +93,7 @@ function installRedisSync() {
87
93
  * @returns {Promise<RedisClient|RedisCluster>} Redis client or cluster instance
88
94
  */
89
95
  async function createConnection(config) {
90
- const { needsInstall } = checkRedisInstallation();
96
+ const { needsInstall } = await checkRedisInstallation();
91
97
 
92
98
  if (needsInstall) {
93
99
  const installed = installRedisSync();
@@ -119,46 +125,49 @@ async function createConnection(config) {
119
125
  let connectionAttempts = 0;
120
126
  const maxRetries = options.maxRetries || 3;
121
127
 
122
- // Create a promise that will resolve when connected or reject on fatal errors
123
- const connectionPromise = new Promise((resolve, reject) => {
124
- connectionTimeout = setTimeout(() => {
125
- if (!hasConnected) {
126
- client.quit().catch(() => {});
127
- reject(new TejError(500, 'Redis connection timeout'));
128
- }
129
- }, options.connectTimeout || 10000);
130
-
131
- client.on('error', (err) => {
132
- logger.error(`Redis connection error: ${err}`, true);
133
- if (!hasConnected && connectionAttempts >= maxRetries) {
134
- clearTimeout(connectionTimeout);
135
- client.quit().catch(() => {});
136
- reject(
137
- new TejError(
138
- 500,
139
- `Redis connection failed after ${maxRetries} attempts: ${err.message}`,
140
- ),
141
- );
142
- }
143
- connectionAttempts++;
144
- });
128
+ const {
129
+ promise: connectionPromise,
130
+ resolve: resolveConnection,
131
+ reject: rejectConnection,
132
+ } = Promise.withResolvers();
145
133
 
146
- client.on('connect', () => {
147
- hasConnected = true;
134
+ connectionTimeout = setTimeout(() => {
135
+ if (!hasConnected) {
136
+ client.quit().catch(() => {});
137
+ rejectConnection(new TejError(500, 'Redis connection timeout'));
138
+ }
139
+ }, options.connectTimeout || 10000);
140
+
141
+ client.on('error', (err) => {
142
+ logger.error(`Redis connection error: ${err}`, true);
143
+ if (!hasConnected && connectionAttempts >= maxRetries) {
148
144
  clearTimeout(connectionTimeout);
149
- logger.info(
150
- `Redis connected on ${client?.options?.url ?? client?.options?.socket?.host}`,
145
+ client.quit().catch(() => {});
146
+ rejectConnection(
147
+ new TejError(
148
+ 500,
149
+ `Redis connection failed after ${maxRetries} attempts: ${err.message}`,
150
+ ),
151
151
  );
152
- });
152
+ }
153
+ connectionAttempts++;
154
+ });
153
155
 
154
- client.on('ready', () => {
155
- logger.info('Redis ready');
156
- resolve(client);
157
- });
156
+ client.on('connect', () => {
157
+ hasConnected = true;
158
+ clearTimeout(connectionTimeout);
159
+ logger.info(
160
+ `Redis connected on ${client?.options?.url ?? client?.options?.socket?.host}`,
161
+ );
162
+ });
158
163
 
159
- client.on('end', () => {
160
- logger.info('Redis connection closed');
161
- });
164
+ client.on('ready', () => {
165
+ logger.info('Redis ready');
166
+ resolveConnection(client);
167
+ });
168
+
169
+ client.on('end', () => {
170
+ logger.info('Redis connection closed');
162
171
  });
163
172
 
164
173
  await client.connect();
@@ -110,15 +110,22 @@ These options configure the `tejas generate:docs` CLI command and the auto-docum
110
110
 
111
111
  ### Error handling (LLM-inferred errors)
112
112
 
113
- When [LLM-inferred error codes and messages](./error-handling.md#llm-inferred-errors) are enabled, the **`errors.llm`** block configures the LLM used for inferring status code and message when you call `ammo.throw()` without explicit code or message. Unset values fall back to `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`. You can also enable (and optionally set connection options) by calling **`app.withLLMErrors(config?)`** before `takeoff()` — e.g. `app.withLLMErrors()` to use env/config for baseURL, apiKey, model, or `app.withLLMErrors({ baseURL, apiKey, model, messageType })` to override in code.
114
-
115
- | Config Key | Env Variable | Type | Default | Description |
116
- | ------------------------ | ------------------------------------------------ | ---------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
117
- | `errors.llm.enabled` | `ERRORS_LLM_ENABLED` or `LLM_*` (for connection) | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` |
118
- | `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for error inference |
119
- | `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for error inference |
120
- | `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for error inference |
121
- | `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE` | `"endUser"` \| `"developer"` | `"endUser"` | Default tone for LLM-generated message: `endUser` (safe for clients) or `developer` (technical detail). Overridable per `ammo.throw()` call. |
113
+ When [LLM-inferred error codes and messages](./error-handling.md#llm-inferred-errors) are enabled, the **`errors.llm`** block configures the LLM used for inferring status code and message when you call `ammo.throw()` without explicit code or message. Unset values fall back to `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`. You can also enable (and optionally set connection options) by calling **`app.withLLMErrors(config?)`** before `takeoff()` — e.g. `app.withLLMErrors()` to use env/config for baseURL, apiKey, model, or `app.withLLMErrors({ baseURL, apiKey, model, messageType, mode, ... })` to override in code.
114
+
115
+ | Config Key | Env Variable | Type | Default | Description |
116
+ | ------------------------ | ----------------------------------------------- | ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `errors.llm.enabled` | `ERRORS_LLM_ENABLED` | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` and framework-caught errors. |
118
+ | `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | — | LLM provider endpoint (e.g. `https://api.openai.com/v1`). Required when enabled. |
119
+ | `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key. Required when enabled. |
120
+ | `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | — | LLM model name (e.g. `gpt-4o-mini`). Required when enabled. |
121
+ | `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE` | `"endUser"` \| `"developer"` | `"endUser"` | Default tone for LLM-generated messages. `endUser` is safe for clients; `developer` includes technical detail. Overridable per `ammo.throw()` call. |
122
+ | `errors.llm.mode` | `ERRORS_LLM_MODE` or `LLM_MODE` | `"sync"` \| `"async"` | `"sync"` | `sync` blocks the HTTP response until the LLM returns. `async` sends an immediate 500 and runs the LLM in the background, dispatching the result to the configured channel. |
123
+ | `errors.llm.timeout` | `ERRORS_LLM_TIMEOUT` or `LLM_TIMEOUT` | number (ms) | `10000` | Maximum time in milliseconds to wait for an LLM response before aborting with a timeout error. |
124
+ | `errors.llm.channel` | `ERRORS_LLM_CHANNEL` or `LLM_CHANNEL` | `"console"` \| `"log"` \| `"both"` | `"console"` | Output channel for async mode results. `console` pretty-prints to the terminal; `log` appends JSONL to the log file; `both` does both. Only applies when `mode` is `async`. |
125
+ | `errors.llm.logFile` | `ERRORS_LLM_LOG_FILE` | string (path) | `"./errors.llm.log"` | Path for the JSONL log file used by the `log` and `both` channels. |
126
+ | `errors.llm.rateLimit` | `ERRORS_LLM_RATE_LIMIT` or `LLM_RATE_LIMIT` | number | `10` | Maximum number of LLM calls allowed per minute across all requests. When exceeded, a generic 500 is returned (sync) or dispatched with a `rateLimited` flag (async). Cached results do not count against this limit. |
127
+ | `errors.llm.cache` | `ERRORS_LLM_CACHE` | boolean | `true` | Cache LLM results by throw site (file + line) and error message. Repeated errors at the same location reuse the cached result without making another LLM call. |
128
+ | `errors.llm.cacheTTL` | `ERRORS_LLM_CACHE_TTL` | number (ms) | `3600000` | How long cached results are reused (default 1 hour). After expiry the same error will trigger a fresh LLM call. |
122
129
 
123
130
  When enabled, the same behaviour applies whether you call `ammo.throw()` or the framework calls it when it catches an error — one mechanism, no separate config.
124
131
 
@@ -162,7 +169,14 @@ Create a `tejas.config.json` in your project root:
162
169
  "enabled": true,
163
170
  "baseURL": "https://api.openai.com/v1",
164
171
  "model": "gpt-4o-mini",
165
- "messageType": "endUser"
172
+ "messageType": "endUser",
173
+ "mode": "async",
174
+ "timeout": 10000,
175
+ "channel": "both",
176
+ "logFile": "./errors.llm.log",
177
+ "rateLimit": 10,
178
+ "cache": true,
179
+ "cacheTTL": 3600000
166
180
  }
167
181
  }
168
182
  }