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.
- package/README.md +1 -12
- 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/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +7 -2
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +382 -0
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +19 -22
- package/rate-limit/index.test.js +93 -0
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- 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 +138 -12
- 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/channels.test.js +148 -0
- package/server/errors/channels/index.js +1 -1
- package/server/errors/llm-cache.js +1 -1
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +1 -1
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +1 -1
- package/server/targets/registry.js +3 -3
- package/server/targets/registry.test.js +108 -0
- package/te.js +233 -183
- 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 +74 -8
- package/utils/request-logger.js +49 -3
- package/utils/startup.js +80 -0
- package/database/index.js +0 -165
- package/database/mongodb.js +0 -146
- package/database/redis.js +0 -201
- package/docs/database.md +0 -390
package/cli/docs-command.js
CHANGED
|
@@ -32,25 +32,25 @@ function mask(value) {
|
|
|
32
32
|
|
|
33
33
|
function ask(question, fallback = '') {
|
|
34
34
|
const hint = fallback ? c.dim(` (${fallback})`) : '';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
35
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
36
|
+
rl.question(`${c.cyan('?')} ${question}${hint}${c.dim(': ')}`, (answer) => {
|
|
37
|
+
resolve(answer.trim() || fallback);
|
|
39
38
|
});
|
|
39
|
+
return promise;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function askYesNo(question, fallback = false) {
|
|
43
43
|
const hint = fallback ? 'Y/n' : 'y/N';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
45
|
+
rl.question(
|
|
46
|
+
`${c.cyan('?')} ${question} ${c.dim(`(${hint})`)}${c.dim(': ')}`,
|
|
47
|
+
(answer) => {
|
|
48
|
+
const val = answer.trim().toLowerCase();
|
|
49
|
+
if (!val) return resolve(fallback);
|
|
50
|
+
resolve(val === 'y' || val === 'yes');
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
return promise;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async function loadTargetFiles(dirTargets = 'targets') {
|
|
@@ -88,13 +88,15 @@ async function loadTargetFiles(dirTargets = 'targets') {
|
|
|
88
88
|
function getDocsOptionsFromConfig(config = {}) {
|
|
89
89
|
const docs = config.docs || config.generateDocs || {};
|
|
90
90
|
const e = process.env;
|
|
91
|
-
const baseURL =
|
|
91
|
+
const baseURL =
|
|
92
|
+
docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
|
|
92
93
|
const apiKey = docs.llm?.apiKey ?? e.LLM_API_KEY ?? e.OPENAI_API_KEY;
|
|
93
94
|
const model = docs.llm?.model ?? e.LLM_MODEL ?? 'gpt-4o-mini';
|
|
94
95
|
if (!apiKey && !e.OPENAI_API_KEY) {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
|
-
const dirTargets =
|
|
98
|
+
const dirTargets =
|
|
99
|
+
docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
|
|
98
100
|
const output = docs.output ?? './openapi.json';
|
|
99
101
|
const title = docs.title ?? 'API';
|
|
100
102
|
const version = docs.version ?? '1.0.0';
|
|
@@ -116,7 +118,7 @@ function getDocsOptionsFromConfig(config = {}) {
|
|
|
116
118
|
* @returns {Promise<void>}
|
|
117
119
|
*/
|
|
118
120
|
export async function runDocsCommandCI() {
|
|
119
|
-
const config = loadConfigFile();
|
|
121
|
+
const config = await loadConfigFile();
|
|
120
122
|
const options = getDocsOptionsFromConfig(config);
|
|
121
123
|
if (!options) {
|
|
122
124
|
console.error(
|
|
@@ -128,7 +130,9 @@ export async function runDocsCommandCI() {
|
|
|
128
130
|
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
129
131
|
const fileCount = await loadTargetFiles(dirTargets);
|
|
130
132
|
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
131
|
-
process.stdout.write(
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`,
|
|
135
|
+
);
|
|
132
136
|
if (endpointCount === 0) {
|
|
133
137
|
console.log(c.yellow(' No endpoints found. Skipping doc generation.\n'));
|
|
134
138
|
return;
|
|
@@ -156,19 +160,19 @@ export async function runDocsCommandCI() {
|
|
|
156
160
|
* @returns {Promise<string[]>} e.g. ['refs/heads/main', 'refs/heads/feature/x']
|
|
157
161
|
*/
|
|
158
162
|
function readPrePushRefs() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
163
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
164
|
+
const chunks = [];
|
|
165
|
+
stdin.on('data', (chunk) => chunks.push(chunk));
|
|
166
|
+
stdin.on('end', () => {
|
|
167
|
+
const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
|
|
168
|
+
const refs = [];
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const parts = lines[i].split(/\s+/);
|
|
171
|
+
if (parts.length >= 4) refs.push(parts[2]);
|
|
172
|
+
}
|
|
173
|
+
resolve(refs);
|
|
171
174
|
});
|
|
175
|
+
return promise;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
/**
|
|
@@ -178,7 +182,7 @@ function readPrePushRefs() {
|
|
|
178
182
|
* Configure via tejas.config.json docs.productionBranch or env DOCS_PRODUCTION_BRANCH (default: main).
|
|
179
183
|
*/
|
|
180
184
|
export async function runDocsOnPush() {
|
|
181
|
-
const config = loadConfigFile();
|
|
185
|
+
const config = await loadConfigFile();
|
|
182
186
|
const docs = config.docs || config.generateDocs || {};
|
|
183
187
|
const productionBranch =
|
|
184
188
|
docs.productionBranch ?? process.env.DOCS_PRODUCTION_BRANCH ?? 'main';
|
|
@@ -186,7 +190,11 @@ export async function runDocsOnPush() {
|
|
|
186
190
|
const productionRef = `refs/heads/${productionBranch}`;
|
|
187
191
|
const isPushingToProduction = remoteRefs.some((ref) => ref === productionRef);
|
|
188
192
|
if (!isPushingToProduction) return;
|
|
189
|
-
console.log(
|
|
193
|
+
console.log(
|
|
194
|
+
c.dim(
|
|
195
|
+
` Docs: pushing to ${productionBranch} — generating documentation...\n`,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
190
198
|
await runDocsCommandCI();
|
|
191
199
|
}
|
|
192
200
|
|
|
@@ -207,13 +215,13 @@ function serveDocsPreview(spec, port = 3333) {
|
|
|
207
215
|
res.writeHead(404);
|
|
208
216
|
res.end('Not found');
|
|
209
217
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
219
|
+
server.listen(port, () => resolve(server));
|
|
220
|
+
return promise;
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
export async function runDocsCommand() {
|
|
216
|
-
const config = loadConfigFile();
|
|
224
|
+
const config = await loadConfigFile();
|
|
217
225
|
const e = process.env;
|
|
218
226
|
|
|
219
227
|
console.log();
|
|
@@ -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/docs/README.md
CHANGED
|
@@ -18,8 +18,7 @@ Welcome to the documentation for **Tejas** — a Node.js framework for building
|
|
|
18
18
|
|
|
19
19
|
### Features
|
|
20
20
|
|
|
21
|
-
- [
|
|
22
|
-
- [Rate Limiting](./rate-limiting.md) — Three algorithms, two storage backends, custom headers
|
|
21
|
+
- [Rate Limiting](./rate-limiting.md) — Three algorithms, memory or Redis storage, custom headers
|
|
23
22
|
- [File Uploads](./file-uploads.md) — Single and multiple file handling with validation
|
|
24
23
|
|
|
25
24
|
### Tooling
|