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
|
@@ -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
|
-
|
|
18
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
19
|
+
setTimeout(resolve, ms);
|
|
20
|
+
return promise;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
constructor() {
|
package/database/mongodb.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
23
|
+
const packageJson = JSON.parse(
|
|
24
|
+
await fs.promises.readFile(packageJsonPath, 'utf8'),
|
|
25
|
+
);
|
|
25
26
|
const inPackageJson = !!packageJson.dependencies?.mongoose;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
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')
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
const packageJson = JSON.parse(
|
|
16
|
+
await fs.promises.readFile(packageJsonPath, 'utf8'),
|
|
17
|
+
);
|
|
17
18
|
const inPackageJson = !!packageJson.dependencies?.redis;
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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();
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
116
|
-
| ------------------------ |
|
|
117
|
-
| `errors.llm.enabled` | `ERRORS_LLM_ENABLED`
|
|
118
|
-
| `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL`
|
|
119
|
-
| `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY`
|
|
120
|
-
| `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL`
|
|
121
|
-
| `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE`
|
|
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
|
}
|