te.js 2.0.3 → 2.1.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 (68) hide show
  1. package/README.md +197 -187
  2. package/auto-docs/analysis/handler-analyzer.js +58 -58
  3. package/auto-docs/analysis/source-resolver.js +101 -101
  4. package/auto-docs/constants.js +37 -37
  5. package/auto-docs/docs-llm/index.js +7 -0
  6. package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
  7. package/auto-docs/{llm → docs-llm}/provider.js +132 -187
  8. package/auto-docs/index.js +146 -146
  9. package/auto-docs/openapi/endpoint-processor.js +277 -277
  10. package/auto-docs/openapi/generator.js +107 -107
  11. package/auto-docs/openapi/level3.js +131 -131
  12. package/auto-docs/openapi/spec-builders.js +244 -244
  13. package/auto-docs/ui/docs-ui.js +186 -186
  14. package/auto-docs/utils/logger.js +17 -17
  15. package/auto-docs/utils/strip-usage.js +10 -10
  16. package/cli/docs-command.js +315 -315
  17. package/cli/fly-command.js +71 -71
  18. package/cli/index.js +56 -56
  19. package/database/index.js +165 -165
  20. package/database/mongodb.js +146 -146
  21. package/database/redis.js +201 -201
  22. package/docs/README.md +36 -36
  23. package/docs/ammo.md +362 -362
  24. package/docs/api-reference.md +490 -489
  25. package/docs/auto-docs.md +216 -215
  26. package/docs/cli.md +152 -152
  27. package/docs/configuration.md +275 -233
  28. package/docs/database.md +390 -391
  29. package/docs/error-handling.md +438 -417
  30. package/docs/file-uploads.md +333 -334
  31. package/docs/getting-started.md +214 -215
  32. package/docs/middleware.md +355 -356
  33. package/docs/rate-limiting.md +393 -394
  34. package/docs/routing.md +302 -302
  35. package/package.json +62 -62
  36. package/rate-limit/algorithms/fixed-window.js +141 -141
  37. package/rate-limit/algorithms/sliding-window.js +147 -147
  38. package/rate-limit/algorithms/token-bucket.js +115 -115
  39. package/rate-limit/base.js +165 -165
  40. package/rate-limit/index.js +147 -147
  41. package/rate-limit/storage/base.js +104 -104
  42. package/rate-limit/storage/memory.js +101 -101
  43. package/rate-limit/storage/redis.js +88 -88
  44. package/server/ammo/body-parser.js +220 -220
  45. package/server/ammo/dispatch-helper.js +103 -103
  46. package/server/ammo/enhancer.js +57 -57
  47. package/server/ammo.js +454 -356
  48. package/server/endpoint.js +97 -74
  49. package/server/error.js +9 -9
  50. package/server/errors/code-context.js +125 -0
  51. package/server/errors/llm-error-service.js +140 -0
  52. package/server/files/helper.js +33 -33
  53. package/server/files/uploader.js +143 -143
  54. package/server/handler.js +158 -113
  55. package/server/target.js +185 -175
  56. package/server/targets/middleware-validator.js +22 -22
  57. package/server/targets/path-validator.js +21 -21
  58. package/server/targets/registry.js +160 -160
  59. package/server/targets/shoot-validator.js +21 -21
  60. package/te.js +428 -363
  61. package/utils/auto-register.js +17 -17
  62. package/utils/configuration.js +64 -64
  63. package/utils/errors-llm-config.js +84 -0
  64. package/utils/request-logger.js +43 -43
  65. package/utils/status-codes.js +82 -82
  66. package/utils/tejas-entrypoint-html.js +18 -18
  67. package/auto-docs/llm/index.js +0 -6
  68. package/auto-docs/llm/parse.js +0 -88
@@ -1,143 +1,143 @@
1
- import { filesize } from 'filesize';
2
- import fs from 'node:fs';
3
- import TejError from './../error.js';
4
- import { extAndType, extract, paths } from './helper.js';
5
-
6
- class TejFileUploader {
7
- /*
8
- * @param {Object} options
9
- * @param {string} options.destination - Destination to upload file to
10
- * @param {string} options.name - Name of the file
11
- * @param {number} options.maxFileSize - Maximum file size in bytes
12
- */
13
- constructor(options = {}) {
14
- this.destination = options.destination;
15
- this.name = options.name;
16
- this.maxFileSize = options.maxFileSize;
17
- }
18
-
19
- file() {
20
- const keys = [...arguments];
21
- return async (ammo, next) => {
22
- if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
23
- return next();
24
-
25
- const payload = ammo.payload;
26
- const updatedPayload = {};
27
-
28
- for (const part in payload) {
29
- const obj = payload[part];
30
- const contentDisposition = obj.headers['content-disposition'];
31
-
32
- const { ext, type } = extAndType(obj);
33
-
34
- const key = extract(contentDisposition, 'name');
35
- if (!key) continue;
36
-
37
- if (!ext || ext === 'txt') {
38
- updatedPayload[key] = obj.value;
39
- } else {
40
- if (!keys.includes(key)) continue;
41
-
42
- const filename = extract(contentDisposition, 'filename');
43
- if (!filename) continue;
44
-
45
- const { dir, absolute, relative } = paths(this.destination, filename);
46
- const size = filesize(obj.value.length,
47
- { output: 'object', round: 0 });
48
- const maxSize = filesize(this.maxFileSize,
49
- { output: 'object', round: 0 });
50
- if (this.maxFileSize && obj.value.length > this.maxFileSize)
51
- throw new TejError(413,
52
- `File size exceeds ${maxSize.value} ${maxSize.symbol}`);
53
-
54
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
- fs.writeFileSync(absolute, obj.value, 'binary');
56
-
57
- updatedPayload[key] = {
58
- filename,
59
- extension: ext,
60
- path: {
61
- absolute: absolute,
62
- relative: relative
63
- },
64
- mimetype: type,
65
- size
66
- };
67
- }
68
- }
69
-
70
- ammo.payload = updatedPayload;
71
- next();
72
- };
73
- }
74
-
75
- files() {
76
- const keys = [...arguments];
77
- return async (ammo, next) => {
78
- if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
79
- return next();
80
-
81
- const payload = ammo.payload;
82
- const updatedPayload = {};
83
- const files = [];
84
-
85
- for (const part in payload) {
86
- const obj = payload[part];
87
- const contentDisposition = obj.headers['content-disposition'];
88
-
89
- const { ext, type } = extAndType(obj);
90
- if (!ext) continue;
91
-
92
- const key = extract(contentDisposition, 'name');
93
- if (ext === 'txt') {
94
- updatedPayload[key] = obj.value;
95
- } else {
96
- if (!keys.includes(key)) continue;
97
-
98
- const filename = extract(contentDisposition, 'filename');
99
- if (!filename) continue;
100
-
101
- const { dir, absolute, relative } = paths(this.destination, filename);
102
- const size = filesize(obj.value.length,
103
- { output: 'object', round: 0 });
104
- const maxSize = filesize(this.maxFileSize,
105
- { output: 'object', round: 0 });
106
- if (this.maxFileSize && obj.value.length > this.maxFileSize) {
107
- throw new TejError(413,
108
- `File size exceeds ${maxSize.value} ${maxSize.symbol}`);
109
- }
110
-
111
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
112
- fs.writeFileSync(absolute, obj.value, 'binary');
113
-
114
- files.push({
115
- key,
116
- filename,
117
- path: {
118
- absolute: absolute,
119
- relative: relative
120
- },
121
- mimetype: type,
122
- size
123
- });
124
- }
125
- }
126
-
127
- const groupedFilesByKey = files.reduce((acc, file) => {
128
- if (!acc[file.key]) acc[file.key] = [];
129
- acc[file.key].push(file);
130
- return acc;
131
- }, {});
132
-
133
- for (const key in groupedFilesByKey) {
134
- updatedPayload[key] = groupedFilesByKey[key];
135
- }
136
-
137
- ammo.payload = updatedPayload;
138
- next();
139
- };
140
- }
141
- }
142
-
143
- export default TejFileUploader;
1
+ import { filesize } from 'filesize';
2
+ import fs from 'node:fs';
3
+ import TejError from './../error.js';
4
+ import { extAndType, extract, paths } from './helper.js';
5
+
6
+ class TejFileUploader {
7
+ /*
8
+ * @param {Object} options
9
+ * @param {string} options.destination - Destination to upload file to
10
+ * @param {string} options.name - Name of the file
11
+ * @param {number} options.maxFileSize - Maximum file size in bytes
12
+ */
13
+ constructor(options = {}) {
14
+ this.destination = options.destination;
15
+ this.name = options.name;
16
+ this.maxFileSize = options.maxFileSize;
17
+ }
18
+
19
+ file() {
20
+ const keys = [...arguments];
21
+ return async (ammo, next) => {
22
+ if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
23
+ return next();
24
+
25
+ const payload = ammo.payload;
26
+ const updatedPayload = {};
27
+
28
+ for (const part in payload) {
29
+ const obj = payload[part];
30
+ const contentDisposition = obj.headers['content-disposition'];
31
+
32
+ const { ext, type } = extAndType(obj);
33
+
34
+ const key = extract(contentDisposition, 'name');
35
+ if (!key) continue;
36
+
37
+ if (!ext || ext === 'txt') {
38
+ updatedPayload[key] = obj.value;
39
+ } else {
40
+ if (!keys.includes(key)) continue;
41
+
42
+ const filename = extract(contentDisposition, 'filename');
43
+ if (!filename) continue;
44
+
45
+ const { dir, absolute, relative } = paths(this.destination, filename);
46
+ const size = filesize(obj.value.length,
47
+ { output: 'object', round: 0 });
48
+ const maxSize = filesize(this.maxFileSize,
49
+ { output: 'object', round: 0 });
50
+ if (this.maxFileSize && obj.value.length > this.maxFileSize)
51
+ throw new TejError(413,
52
+ `File size exceeds ${maxSize.value} ${maxSize.symbol}`);
53
+
54
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
+ fs.writeFileSync(absolute, obj.value, 'binary');
56
+
57
+ updatedPayload[key] = {
58
+ filename,
59
+ extension: ext,
60
+ path: {
61
+ absolute: absolute,
62
+ relative: relative
63
+ },
64
+ mimetype: type,
65
+ size
66
+ };
67
+ }
68
+ }
69
+
70
+ ammo.payload = updatedPayload;
71
+ next();
72
+ };
73
+ }
74
+
75
+ files() {
76
+ const keys = [...arguments];
77
+ return async (ammo, next) => {
78
+ if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
79
+ return next();
80
+
81
+ const payload = ammo.payload;
82
+ const updatedPayload = {};
83
+ const files = [];
84
+
85
+ for (const part in payload) {
86
+ const obj = payload[part];
87
+ const contentDisposition = obj.headers['content-disposition'];
88
+
89
+ const { ext, type } = extAndType(obj);
90
+ if (!ext) continue;
91
+
92
+ const key = extract(contentDisposition, 'name');
93
+ if (ext === 'txt') {
94
+ updatedPayload[key] = obj.value;
95
+ } else {
96
+ if (!keys.includes(key)) continue;
97
+
98
+ const filename = extract(contentDisposition, 'filename');
99
+ if (!filename) continue;
100
+
101
+ const { dir, absolute, relative } = paths(this.destination, filename);
102
+ const size = filesize(obj.value.length,
103
+ { output: 'object', round: 0 });
104
+ const maxSize = filesize(this.maxFileSize,
105
+ { output: 'object', round: 0 });
106
+ if (this.maxFileSize && obj.value.length > this.maxFileSize) {
107
+ throw new TejError(413,
108
+ `File size exceeds ${maxSize.value} ${maxSize.symbol}`);
109
+ }
110
+
111
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
112
+ fs.writeFileSync(absolute, obj.value, 'binary');
113
+
114
+ files.push({
115
+ key,
116
+ filename,
117
+ path: {
118
+ absolute: absolute,
119
+ relative: relative
120
+ },
121
+ mimetype: type,
122
+ size
123
+ });
124
+ }
125
+ }
126
+
127
+ const groupedFilesByKey = files.reduce((acc, file) => {
128
+ if (!acc[file.key]) acc[file.key] = [];
129
+ acc[file.key].push(file);
130
+ return acc;
131
+ }, {});
132
+
133
+ for (const key in groupedFilesByKey) {
134
+ updatedPayload[key] = groupedFilesByKey[key];
135
+ }
136
+
137
+ ammo.payload = updatedPayload;
138
+ next();
139
+ };
140
+ }
141
+ }
142
+
143
+ export default TejFileUploader;
package/server/handler.js CHANGED
@@ -1,113 +1,158 @@
1
- import { env } from 'tej-env';
2
- import TejLogger from 'tej-logger';
3
- import logHttpRequest from '../utils/request-logger.js';
4
-
5
- import Ammo from './ammo.js';
6
- import TejError from './error.js';
7
- import targetRegistry from './targets/registry.js';
8
-
9
- const errorLogger = new TejLogger('Tejas.Exception');
10
-
11
- /**
12
- * Executes the middleware and handler chain for a given target.
13
- *
14
- * @param {Object} target - The target endpoint object.
15
- * @param {Ammo} ammo - The Ammo instance containing request and response objects.
16
- * @returns {Promise<void>} A promise that resolves when the chain execution is complete.
17
- */
18
- const executeChain = async (target, ammo) => {
19
- let i = 0;
20
-
21
- const chain = targetRegistry.globalMiddlewares.concat(
22
- target.getMiddlewares(),
23
- );
24
- chain.push(target.getHandler());
25
-
26
- const next = async () => {
27
- // Check if response has already been sent (e.g., by passport.authenticate redirect)
28
- if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
29
- return;
30
- }
31
-
32
- const middleware = chain[i];
33
- i++;
34
-
35
- const args =
36
- middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
37
-
38
- try {
39
- const result = await middleware(...args);
40
-
41
- // Check again after middleware execution (passport might have redirected)
42
- if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
43
- return;
44
- }
45
-
46
- // If middleware returned a promise that resolved, continue chain
47
- if (result && typeof result.then === 'function') {
48
- await result;
49
- // Check one more time after promise resolution
50
- if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
51
- return;
52
- }
53
- }
54
- } catch (err) {
55
- // Only handle error if response hasn't been sent
56
- if (!ammo.res.headersSent && !ammo.res.writableEnded && !ammo.res.finished) {
57
- errorHandler(ammo, err);
58
- }
59
- }
60
- };
61
-
62
- await next();
63
- };
64
-
65
- /**
66
- * Handles errors by logging them and sending an appropriate response.
67
- *
68
- * @param {Ammo} ammo - The Ammo instance containing request and response objects.
69
- * @param {Error} err - The error object to handle.
70
- */
71
- const errorHandler = (ammo, err) => {
72
- if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
73
-
74
- if (err instanceof TejError) return ammo.throw(err);
75
- return ammo.throw(err);
76
- };
77
-
78
- /**
79
- * Main request handler function.
80
- *
81
- * @param {http.IncomingMessage} req - The HTTP request object.
82
- * @param {http.ServerResponse} res - The HTTP response object.
83
- * @returns {Promise<void>} A promise that resolves when the request handling is complete.
84
- */
85
- const handler = async (req, res) => {
86
- const url = req.url.split('?')[0];
87
- const match = targetRegistry.aim(url);
88
- const ammo = new Ammo(req, res);
89
-
90
- try {
91
- if (match && match.target) {
92
- await ammo.enhance();
93
-
94
- // Add route parameters to ammo.payload
95
- if (match.params && Object.keys(match.params).length > 0) {
96
- Object.assign(ammo.payload, match.params);
97
- }
98
-
99
- if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
100
- await executeChain(match.target, ammo);
101
- } else {
102
- if (req.url === '/') {
103
- ammo.defaultEntry();
104
- } else {
105
- errorHandler(ammo, new TejError(404, `URL not found: ${url}`));
106
- }
107
- }
108
- } catch (err) {
109
- errorHandler(ammo, err);
110
- }
111
- };
112
-
113
- export default handler;
1
+ import { env } from 'tej-env';
2
+ import TejLogger from 'tej-logger';
3
+ import logHttpRequest from '../utils/request-logger.js';
4
+
5
+ import Ammo from './ammo.js';
6
+ import TejError from './error.js';
7
+ import targetRegistry from './targets/registry.js';
8
+
9
+ const errorLogger = new TejLogger('Tejas.Exception');
10
+
11
+ const DEFAULT_ALLOWED_METHODS = [
12
+ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
13
+ ];
14
+
15
+ /**
16
+ * Returns the set of allowed HTTP methods (configurable via allowedMethods in tejas.config.json or ALLOWEDMETHODS env).
17
+ * @returns {Set<string>}
18
+ */
19
+ const getAllowedMethods = () => {
20
+ const raw = env('ALLOWEDMETHODS');
21
+ if (raw == null) return new Set(DEFAULT_ALLOWED_METHODS);
22
+ const arr = Array.isArray(raw)
23
+ ? raw
24
+ : (typeof raw === 'string' ? raw.split(',').map((s) => s.trim()) : []);
25
+ const normalized = arr.map((m) => String(m).toUpperCase()).filter(Boolean);
26
+ return normalized.length > 0 ? new Set(normalized) : new Set(DEFAULT_ALLOWED_METHODS);
27
+ };
28
+
29
+ /**
30
+ * Executes the middleware and handler chain for a given target.
31
+ *
32
+ * @param {Object} target - The target endpoint object.
33
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
34
+ * @returns {Promise<void>} A promise that resolves when the chain execution is complete.
35
+ */
36
+ const executeChain = async (target, ammo) => {
37
+ let i = 0;
38
+
39
+ const chain = targetRegistry.globalMiddlewares.concat(
40
+ target.getMiddlewares(),
41
+ );
42
+ chain.push(target.getHandler());
43
+
44
+ const next = async () => {
45
+ // Check if response has already been sent (e.g., by passport.authenticate redirect)
46
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
47
+ return;
48
+ }
49
+
50
+ const middleware = chain[i];
51
+ i++;
52
+
53
+ const args =
54
+ middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
55
+
56
+ try {
57
+ const result = await middleware(...args);
58
+
59
+ // Check again after middleware execution (passport might have redirected)
60
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
61
+ return;
62
+ }
63
+
64
+ // If middleware returned a promise that resolved, continue chain
65
+ if (result && typeof result.then === 'function') {
66
+ await result;
67
+ // Check one more time after promise resolution
68
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
69
+ return;
70
+ }
71
+ }
72
+ } catch (err) {
73
+ // Only handle error if response hasn't been sent
74
+ if (!ammo.res.headersSent && !ammo.res.writableEnded && !ammo.res.finished) {
75
+ await errorHandler(ammo, err);
76
+ }
77
+ }
78
+ };
79
+
80
+ await next();
81
+ };
82
+
83
+ /**
84
+ * Handles errors: optional logging (log.exceptions) and sending the response via ammo.throw(err).
85
+ * One mechanism — ammo.throw — takes care of everything (no separate "log then send").
86
+ * When errors.llm.enabled, framework-caught errors get the same LLM-inferred response as explicit ammo.throw().
87
+ * When ammo.throw() returns a Promise (LLM path), waits for it to complete.
88
+ *
89
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
90
+ * @param {Error} err - The error object to handle.
91
+ * @returns {Promise<void>}
92
+ */
93
+ const errorHandler = async (ammo, err) => {
94
+ if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
95
+
96
+ const result = ammo.throw(err);
97
+ if (result != null && typeof result.then === 'function') {
98
+ await result;
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Main request handler function.
104
+ *
105
+ * @param {http.IncomingMessage} req - The HTTP request object.
106
+ * @param {http.ServerResponse} res - The HTTP response object.
107
+ * @returns {Promise<void>} A promise that resolves when the request handling is complete.
108
+ */
109
+ const handler = async (req, res) => {
110
+ const allowedMethods = getAllowedMethods();
111
+ const method = req.method ? String(req.method).toUpperCase() : '';
112
+ if (!method || !allowedMethods.has(method)) {
113
+ res.writeHead(405, {
114
+ 'Content-Type': 'text/plain',
115
+ Allow: [...allowedMethods].join(', '),
116
+ });
117
+ res.end('Method Not Allowed');
118
+ return;
119
+ }
120
+
121
+ const url = req.url.split('?')[0];
122
+ const match = targetRegistry.aim(url);
123
+ const ammo = new Ammo(req, res);
124
+
125
+ try {
126
+ if (match && match.target) {
127
+ await ammo.enhance();
128
+
129
+ const allowedMethods = match.target.getMethods();
130
+ if (allowedMethods != null && allowedMethods.length > 0) {
131
+ const method = ammo.method && String(ammo.method).toUpperCase();
132
+ if (!method || !allowedMethods.includes(method)) {
133
+ ammo.res.setHeader('Allow', allowedMethods.join(', '));
134
+ await errorHandler(ammo, new TejError(405, 'Method Not Allowed'));
135
+ return;
136
+ }
137
+ }
138
+
139
+ // Add route parameters to ammo.payload
140
+ if (match.params && Object.keys(match.params).length > 0) {
141
+ Object.assign(ammo.payload, match.params);
142
+ }
143
+
144
+ if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
145
+ await executeChain(match.target, ammo);
146
+ } else {
147
+ if (req.url === '/') {
148
+ ammo.defaultEntry();
149
+ } else {
150
+ await errorHandler(ammo, new TejError(404, `URL not found: ${url}`));
151
+ }
152
+ }
153
+ } catch (err) {
154
+ await errorHandler(ammo, err);
155
+ }
156
+ };
157
+
158
+ export default handler;