te.js 2.1.1 → 2.1.3

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.
@@ -15,9 +15,49 @@ const ALL_METHODS = [
15
15
  ];
16
16
 
17
17
  /**
18
- * Detects which HTTP methods the handler checks (e.g. ammo.GET, ammo.POST).
19
- * Matches property access patterns like `.GET`, `ammo.GET`, avoiding false positives
20
- * in strings or unrelated identifiers.
18
+ * Extracts allowed methods from .only('GET'), .only("POST", "PUT"), etc. in source.
19
+ * Returns a non-empty array only when at least one valid quoted method is found;
20
+ * otherwise [] so caller can fall back to other detection.
21
+ *
22
+ * @param {string} src - Handler source (e.g. handler.toString())
23
+ * @returns {string[]} Normalized method names (uppercase, HEAD added when GET present), or []
24
+ */
25
+ function detectOnlyMethods(src) {
26
+ const startMarker = '.only(';
27
+ const start = src.indexOf(startMarker);
28
+ if (start === -1) return [];
29
+
30
+ let depth = 1;
31
+ let pos = start + startMarker.length;
32
+ while (pos < src.length && depth > 0) {
33
+ const ch = src[pos];
34
+ if (ch === '(') depth += 1;
35
+ else if (ch === ')') depth -= 1;
36
+ pos += 1;
37
+ }
38
+ const argsStr = src.slice(start + startMarker.length, pos - 1);
39
+
40
+ // Match quoted method names: 'GET', "POST", etc. Only accept known methods.
41
+ const quotedMethodRe = /['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/gi;
42
+ const seen = new Set();
43
+ let match;
44
+ while ((match = quotedMethodRe.exec(argsStr)) !== null) {
45
+ seen.add(match[1].toUpperCase());
46
+ }
47
+ if (seen.size === 0) return [];
48
+
49
+ const list = [...seen];
50
+ if (list.includes('GET') && !list.includes('HEAD')) {
51
+ list.push('HEAD');
52
+ }
53
+ return list;
54
+ }
55
+
56
+ /**
57
+ * Detects which HTTP methods the handler checks (e.g. ammo.GET, ammo.POST)
58
+ * or restricts via ammo.only('GET'), ammo.only('GET','POST').
59
+ * Prefers .only(...) when present with valid string args; otherwise matches
60
+ * property access like `.GET`, `ammo.GET`.
21
61
  *
22
62
  * When no method checks are found, the endpoint is treated as method-agnostic
23
63
  * and accepts ALL methods (te.js default behavior).
@@ -29,8 +69,10 @@ function detectMethods(handler) {
29
69
  if (typeof handler !== 'function') return [...ALL_METHODS];
30
70
 
31
71
  const src = handler.toString();
32
- const detected = [];
72
+ const onlyMethods = detectOnlyMethods(src);
73
+ if (onlyMethods.length > 0) return onlyMethods;
33
74
 
75
+ const detected = [];
34
76
  for (const m of ALL_METHODS) {
35
77
  // Match property access patterns like .GET, ammo.GET, avoiding
36
78
  // false positives in strings or unrelated identifiers
package/cors/index.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * CORS middleware factory. Handles OPTIONS preflight with 204 and sets CORS response headers.
3
+ *
4
+ * @param {Object} config - CORS configuration
5
+ * @param {string|string[]|((origin: string) => boolean)} [config.origin='*'] - Allowed origin(s): '*' or array of origins or function
6
+ * @param {string[]} [config.methods=['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']] - Allowed methods for Access-Control-Allow-Methods
7
+ * @param {string[]} [config.allowedHeaders=['Content-Type','Authorization']] - Allowed request headers for Access-Control-Allow-Headers
8
+ * @param {boolean} [config.credentials=false] - Access-Control-Allow-Credentials (use with specific origin, not '*')
9
+ * @param {number} [config.maxAge] - Access-Control-Max-Age in seconds for preflight cache
10
+ * @returns {Function} Middleware (ammo, next)
11
+ */
12
+ function corsMiddleware(config = {}) {
13
+ const {
14
+ origin = '*',
15
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
16
+ allowedHeaders = ['Content-Type', 'Authorization'],
17
+ credentials = false,
18
+ maxAge,
19
+ } = config;
20
+
21
+ const methodsList = Array.isArray(methods)
22
+ ? methods.map((m) => String(m).toUpperCase()).join(', ')
23
+ : String(methods);
24
+ const headersList = Array.isArray(allowedHeaders)
25
+ ? allowedHeaders.join(', ')
26
+ : String(allowedHeaders);
27
+
28
+ const resolveOrigin = (requestOrigin) => {
29
+ if (typeof origin === 'function') {
30
+ return origin(requestOrigin) ? requestOrigin || '*' : null;
31
+ }
32
+ if (origin === '*') {
33
+ return credentials ? (requestOrigin || '*') : '*';
34
+ }
35
+ if (Array.isArray(origin)) {
36
+ const normalized = (requestOrigin || '').toLowerCase();
37
+ const allowed = origin.some(
38
+ (o) => String(o).toLowerCase() === normalized,
39
+ );
40
+ return allowed ? requestOrigin : null;
41
+ }
42
+ return String(origin) === (requestOrigin || '') ? requestOrigin : null;
43
+ };
44
+
45
+ return async (ammo, next) => {
46
+ const requestOrigin = ammo.req.headers.origin;
47
+
48
+ const allowOrigin = resolveOrigin(requestOrigin);
49
+ if (allowOrigin != null) {
50
+ ammo.res.setHeader('Access-Control-Allow-Origin', allowOrigin);
51
+ }
52
+ ammo.res.setHeader('Access-Control-Allow-Methods', methodsList);
53
+ ammo.res.setHeader('Access-Control-Allow-Headers', headersList);
54
+ if (credentials) {
55
+ ammo.res.setHeader('Access-Control-Allow-Credentials', 'true');
56
+ }
57
+ if (maxAge != null && Number.isFinite(maxAge)) {
58
+ ammo.res.setHeader('Access-Control-Max-Age', String(maxAge));
59
+ }
60
+
61
+ if (ammo.req.method === 'OPTIONS') {
62
+ ammo.res.writeHead(204);
63
+ ammo.res.end();
64
+ return;
65
+ }
66
+
67
+ await next();
68
+ };
69
+ }
70
+
71
+ export default corsMiddleware;
@@ -59,6 +59,16 @@ app.takeoff();
59
59
  | `log.http_requests` | `LOG_HTTP_REQUESTS` | boolean | `false` | Log incoming HTTP requests (method, path, status, time) |
60
60
  | `log.exceptions` | `LOG_EXCEPTIONS` | boolean | `false` | Log unhandled exceptions and errors |
61
61
 
62
+ ### Developer warnings
63
+
64
+ When an endpoint is called and it has no allowed methods defined (see [Routing — Endpoint Metadata](./routing.md#endpoint-metadata)), the framework logs a warning once per path so you can restrict methods for security (405 and `Allow` header). To disable this warning:
65
+
66
+ | Config Key | Env Variable | Type | Default | Description |
67
+ |------------|-------------|------|---------|-------------|
68
+ | `warn_missing_allowed_methods` | `WARN_MISSING_ALLOWED_METHODS` | boolean/string | *(warn)* | Set to `false` to disable the runtime warning for endpoints without allowed methods. |
69
+
70
+ Example: in `tejas.config.json` use `"warn_missing_allowed_methods": false`, or in `.env` use `WARN_MISSING_ALLOWED_METHODS=false`.
71
+
62
72
  ### Request Body
63
73
 
64
74
  | Config Key | Env Variable | Type | Default | Description |
@@ -178,6 +188,9 @@ LLM_MODEL=gpt-4o-mini
178
188
  # ERRORS_LLM_API_KEY=...
179
189
  # ERRORS_LLM_MODEL=...
180
190
  # ERRORS_LLM_MESSAGE_TYPE=endUser # or "developer" for technical messages
191
+
192
+ # Optional: disable runtime warning for endpoints without allowed methods
193
+ # WARN_MISSING_ALLOWED_METHODS=false
181
194
  ```
182
195
 
183
196
  ## Constructor Options
package/docs/routing.md CHANGED
@@ -214,6 +214,8 @@ target.register('/users', {
214
214
 
215
215
  When metadata is omitted, the auto-docs LLM infers everything from the handler source code.
216
216
 
217
+ If an endpoint has no `methods` in its metadata (and does not use `ammo.only()` to restrict methods), the framework logs a warning the first time that path is called. You can disable this warning via config: set `WARN_MISSING_ALLOWED_METHODS=false` (env) or `warn_missing_allowed_methods: false` in config. See [Configuration — Developer warnings](./configuration.md#developer-warnings).
218
+
217
219
  ## Method-Agnostic Handlers
218
220
 
219
221
  If a handler does not check any method flags (`ammo.GET`, `ammo.POST`, etc.), it is treated as accepting **all HTTP methods**. This is useful for simple endpoints:
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Generic OpenAI-compatible LLM client for te.js.
3
+ * POSTs to {baseURL}/chat/completions; used by auto-docs, error-inference, and future LLM features.
4
+ * No provider-specific npm dependencies — uses fetch() only.
5
+ */
6
+
7
+ const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
8
+ const DEFAULT_MODEL = 'gpt-4o-mini';
9
+
10
+ /**
11
+ * OpenAI-compatible LLM provider. Exposes only constructor and analyze(prompt).
12
+ */
13
+ class LLMProvider {
14
+ constructor(options = {}) {
15
+ this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '');
16
+ this.model = options.model ?? DEFAULT_MODEL;
17
+ this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
18
+ this.options = options;
19
+ }
20
+
21
+ /**
22
+ * Send a prompt to the LLM and return the raw text response and usage.
23
+ * @param {string} prompt
24
+ * @returns {Promise<{ content: string, usage: { prompt_tokens: number, completion_tokens: number, total_tokens: number } }>}
25
+ */
26
+ async analyze(prompt) {
27
+ const url = `${this.baseURL}/chat/completions`;
28
+ const headers = {
29
+ 'Content-Type': 'application/json',
30
+ ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
31
+ };
32
+ const body = {
33
+ model: this.model,
34
+ messages: [{ role: 'user', content: prompt }],
35
+ };
36
+
37
+ const res = await fetch(url, {
38
+ method: 'POST',
39
+ headers,
40
+ body: JSON.stringify(body),
41
+ });
42
+
43
+ if (!res.ok) {
44
+ const text = await res.text();
45
+ throw new Error(`LLM request failed (${res.status}): ${text.slice(0, 300)}`);
46
+ }
47
+
48
+ const data = await res.json();
49
+ const content = data.choices?.[0]?.message?.content ?? '';
50
+ const text = typeof content === 'string' ? content : JSON.stringify(content);
51
+ const rawUsage = data.usage;
52
+ const usage = {
53
+ prompt_tokens: rawUsage?.prompt_tokens ?? 0,
54
+ completion_tokens: rawUsage?.completion_tokens ?? 0,
55
+ total_tokens: rawUsage?.total_tokens ?? (rawUsage?.prompt_tokens ?? 0) + (rawUsage?.completion_tokens ?? 0),
56
+ };
57
+ return { content: text, usage };
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Create an LLM provider from config.
63
+ * @param {object} config - { baseURL?, apiKey?, model? }
64
+ * @returns {LLMProvider}
65
+ */
66
+ function createProvider(config) {
67
+ if (!config || typeof config !== 'object') {
68
+ return new LLMProvider({});
69
+ }
70
+ return new LLMProvider(config);
71
+ }
72
+
73
+ export { LLMProvider, createProvider };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared LLM module for te.js: generic client and parse utilities.
3
+ * Used by auto-docs, error-inference, and future LLM features.
4
+ */
5
+
6
+ export { LLMProvider, createProvider } from './client.js';
7
+ export { extractJSON, extractJSONArray, reconcileOrderedTags } from './parse.js';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Parse JSON from LLM response text (handles markdown code blocks).
3
+ * Shared by auto-docs, error-inference, and other LLM features.
4
+ */
5
+
6
+ /**
7
+ * Extract the first JSON object from a string.
8
+ * @param {string} str - Raw LLM response
9
+ * @returns {object|null}
10
+ */
11
+ export function extractJSON(str) {
12
+ if (!str || typeof str !== 'string') return null;
13
+ const trimmed = str.trim();
14
+ const open = trimmed.indexOf('{');
15
+ if (open === -1) return null;
16
+ let depth = 0;
17
+ let end = -1;
18
+ for (let i = open; i < trimmed.length; i++) {
19
+ if (trimmed[i] === '{') depth++;
20
+ else if (trimmed[i] === '}') {
21
+ depth--;
22
+ if (depth === 0) {
23
+ end = i + 1;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ if (end === -1) return null;
29
+ try {
30
+ return JSON.parse(trimmed.slice(open, end));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Extract the first JSON array from a string.
38
+ * @param {string} str - Raw LLM response
39
+ * @returns {Array|null}
40
+ */
41
+ export function extractJSONArray(str) {
42
+ if (!str || typeof str !== 'string') return null;
43
+ const trimmed = str.trim();
44
+ const open = trimmed.indexOf('[');
45
+ if (open === -1) return null;
46
+ let depth = 0;
47
+ let end = -1;
48
+ for (let i = open; i < trimmed.length; i++) {
49
+ if (trimmed[i] === '[') depth++;
50
+ else if (trimmed[i] === ']') {
51
+ depth--;
52
+ if (depth === 0) {
53
+ end = i + 1;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ if (end === -1) return null;
59
+ try {
60
+ return JSON.parse(trimmed.slice(open, end));
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Reconcile LLM-ordered tag names with actual tag objects. Returns tags in desired order;
68
+ * any tag not in orderedTagNames is appended at the end.
69
+ * @param {string[]} orderedTagNames - Tag names in desired order (from LLM)
70
+ * @param {Array<{ name: string, description?: string }>} tags - Current spec.tags
71
+ * @returns {Array<{ name: string, description?: string }>} Tags reordered
72
+ */
73
+ export function reconcileOrderedTags(orderedTagNames, tags) {
74
+ if (!Array.isArray(tags) || !tags.length) return [];
75
+ if (!Array.isArray(orderedTagNames) || !orderedTagNames.length) return [...tags];
76
+ const byName = new Map(tags.map((t) => [t.name, t]));
77
+ const ordered = [];
78
+ for (const name of orderedTagNames) {
79
+ const tag = byName.get(name);
80
+ if (tag) {
81
+ ordered.push(tag);
82
+ byName.delete(name);
83
+ }
84
+ }
85
+ for (const [, tag] of byName) {
86
+ ordered.push(tag);
87
+ }
88
+ return ordered;
89
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.1.1",
4
- "description": "A nodejs framework",
3
+ "version": "2.1.3",
4
+ "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
7
7
  "bin": {
@@ -30,10 +30,12 @@
30
30
  "files": [
31
31
  "te.js",
32
32
  "cli",
33
+ "cors",
33
34
  "server",
34
35
  "database",
35
36
  "rate-limit",
36
37
  "utils",
38
+ "lib",
37
39
  "auto-docs",
38
40
  "README.md",
39
41
  "docs"
package/server/handler.js CHANGED
@@ -7,6 +7,9 @@ import TejError from './error.js';
7
7
  import targetRegistry from './targets/registry.js';
8
8
 
9
9
  const errorLogger = new TejLogger('Tejas.Exception');
10
+ const logger = new TejLogger('Tejas');
11
+ /** Paths we have already warned about (missing allowed methods). */
12
+ const warnedPaths = new Set();
10
13
 
11
14
  const DEFAULT_ALLOWED_METHODS = [
12
15
  'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
@@ -134,6 +137,12 @@ const handler = async (req, res) => {
134
137
  await errorHandler(ammo, new TejError(405, 'Method Not Allowed'));
135
138
  return;
136
139
  }
140
+ } else if (env('WARN_MISSING_ALLOWED_METHODS') !== 'false') {
141
+ const path = match.target.getPath();
142
+ if (!warnedPaths.has(path)) {
143
+ warnedPaths.add(path);
144
+ logger.warn(`Endpoint missing allowed methods: ${path}`);
145
+ }
137
146
  }
138
147
 
139
148
  // Add route parameters to ammo.payload