te.js 2.1.6 → 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 (43) 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/lib/llm/client.js +6 -1
  13. package/lib/llm/index.js +14 -1
  14. package/lib/llm/parse.test.js +60 -0
  15. package/package.json +3 -1
  16. package/radar/index.js +281 -0
  17. package/rate-limit/index.js +8 -11
  18. package/rate-limit/index.test.js +64 -0
  19. package/server/ammo/body-parser.js +156 -152
  20. package/server/ammo/body-parser.test.js +79 -0
  21. package/server/ammo/enhancer.js +8 -4
  22. package/server/ammo.js +135 -10
  23. package/server/context/request-context.js +51 -0
  24. package/server/context/request-context.test.js +53 -0
  25. package/server/endpoint.js +15 -0
  26. package/server/error.js +56 -3
  27. package/server/error.test.js +45 -0
  28. package/server/errors/channels/channels.test.js +148 -0
  29. package/server/errors/channels/index.js +1 -1
  30. package/server/errors/llm-cache.js +1 -1
  31. package/server/errors/llm-cache.test.js +160 -0
  32. package/server/errors/llm-error-service.js +1 -1
  33. package/server/errors/llm-rate-limiter.test.js +105 -0
  34. package/server/files/uploader.js +38 -26
  35. package/server/handler.js +1 -1
  36. package/server/targets/registry.js +3 -3
  37. package/server/targets/registry.test.js +108 -0
  38. package/te.js +178 -49
  39. package/utils/auto-register.js +1 -1
  40. package/utils/configuration.js +23 -9
  41. package/utils/configuration.test.js +58 -0
  42. package/utils/errors-llm-config.js +11 -8
  43. 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();
package/lib/llm/client.js CHANGED
@@ -20,7 +20,7 @@ class LLMProvider {
20
20
  typeof options.timeout === 'number' && options.timeout > 0
21
21
  ? options.timeout
22
22
  : DEFAULT_TIMEOUT;
23
- this.options = options;
23
+ this.options = Object.freeze({ ...options });
24
24
  }
25
25
 
26
26
  /**
@@ -30,6 +30,11 @@ class LLMProvider {
30
30
  * @returns {Promise<{ content: string, usage: { prompt_tokens: number, completion_tokens: number, total_tokens: number } }>}
31
31
  */
32
32
  async analyze(prompt) {
33
+ if (!prompt || typeof prompt !== 'string') {
34
+ throw new TypeError(
35
+ 'LLMProvider.analyze: prompt must be a non-empty string',
36
+ );
37
+ }
33
38
  const url = `${this.baseURL}/chat/completions`;
34
39
  const headers = {
35
40
  'Content-Type': 'application/json',
package/lib/llm/index.js CHANGED
@@ -3,5 +3,18 @@
3
3
  * Used by auto-docs, error-inference, and future LLM features.
4
4
  */
5
5
 
6
+ /**
7
+ * OpenAI-compatible LLM client.
8
+ * @see {@link ./client.js}
9
+ */
6
10
  export { LLMProvider, createProvider } from './client.js';
7
- export { extractJSON, extractJSONArray, reconcileOrderedTags } from './parse.js';
11
+
12
+ /**
13
+ * JSON parsing utilities for LLM responses.
14
+ * @see {@link ./parse.js}
15
+ */
16
+ export {
17
+ extractJSON,
18
+ extractJSONArray,
19
+ reconcileOrderedTags,
20
+ } from './parse.js';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Unit tests for lib/llm parse utilities (extractJSON, extractJSONArray, reconcileOrderedTags).
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ extractJSON,
7
+ extractJSONArray,
8
+ reconcileOrderedTags,
9
+ } from './index.js';
10
+
11
+ describe('llm/parse', () => {
12
+ describe('extractJSON', () => {
13
+ it('extracts object from plain JSON string', () => {
14
+ const str = '{"name":"Users","description":"CRUD"}';
15
+ expect(extractJSON(str)).toEqual({ name: 'Users', description: 'CRUD' });
16
+ });
17
+ it('extracts first object from text with markdown', () => {
18
+ const str = 'Here is the result:\n```json\n{"summary":"Get item"}\n```';
19
+ expect(extractJSON(str)).toEqual({ summary: 'Get item' });
20
+ });
21
+ it('returns null for empty or no object', () => {
22
+ expect(extractJSON('')).toBeNull();
23
+ expect(extractJSON('no brace here')).toBeNull();
24
+ });
25
+ });
26
+
27
+ describe('extractJSONArray', () => {
28
+ it('extracts array from string', () => {
29
+ const str = '["Users", "Auth", "Health"]';
30
+ expect(extractJSONArray(str)).toEqual(['Users', 'Auth', 'Health']);
31
+ });
32
+ it('returns null when no array', () => {
33
+ expect(extractJSONArray('')).toBeNull();
34
+ expect(extractJSONArray('nothing')).toBeNull();
35
+ });
36
+ });
37
+
38
+ describe('reconcileOrderedTags', () => {
39
+ it('reorders tags by orderedTagNames', () => {
40
+ const tags = [
41
+ { name: 'Health', description: '...' },
42
+ { name: 'Users', description: '...' },
43
+ { name: 'Auth', description: '...' },
44
+ ];
45
+ const ordered = reconcileOrderedTags(['Users', 'Auth', 'Health'], tags);
46
+ expect(ordered.map((t) => t.name)).toEqual(['Users', 'Auth', 'Health']);
47
+ });
48
+ it('appends tags not in orderedTagNames', () => {
49
+ const tags = [{ name: 'Users' }, { name: 'Other' }];
50
+ const ordered = reconcileOrderedTags(['Users'], tags);
51
+ expect(ordered.map((t) => t.name)).toEqual(['Users', 'Other']);
52
+ });
53
+ it('returns copy of tags when orderedTagNames empty', () => {
54
+ const tags = [{ name: 'A' }];
55
+ const ordered = reconcileOrderedTags([], tags);
56
+ expect(ordered).toEqual([{ name: 'A' }]);
57
+ expect(ordered).not.toBe(tags);
58
+ });
59
+ });
60
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
@@ -18,6 +18,7 @@
18
18
  "license": "ISC",
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.12.5",
21
+ "@vitest/coverage-v8": "^4.0.18",
21
22
  "husky": "^9.0.11",
22
23
  "lint-staged": "^15.2.2",
23
24
  "prettier": "3.2.5",
@@ -31,6 +32,7 @@
31
32
  "te.js",
32
33
  "cli",
33
34
  "cors",
35
+ "radar",
34
36
  "server",
35
37
  "database",
36
38
  "rate-limit",