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
package/server/ammo.js
CHANGED
|
@@ -10,6 +10,14 @@ import TejError from './error.js';
|
|
|
10
10
|
import { getErrorsLlmConfig } from '../utils/errors-llm-config.js';
|
|
11
11
|
import { inferErrorFromContext } from './errors/llm-error-service.js';
|
|
12
12
|
import { captureCodeContext } from './errors/code-context.js';
|
|
13
|
+
import {
|
|
14
|
+
getChannels,
|
|
15
|
+
buildPayload,
|
|
16
|
+
dispatchToChannels,
|
|
17
|
+
} from './errors/channels/index.js';
|
|
18
|
+
import TejLogger from 'tej-logger';
|
|
19
|
+
|
|
20
|
+
const logger = new TejLogger('Tejas.Ammo');
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Detect if the value is a throw() options object (per-call overrides).
|
|
@@ -80,6 +88,13 @@ class Ammo {
|
|
|
80
88
|
|
|
81
89
|
// Response related data
|
|
82
90
|
this.dispatchedData = undefined;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolved error info stashed after ammo.throw() completes.
|
|
94
|
+
* Read by the radar middleware on res.finish to populate error tracking.
|
|
95
|
+
* @type {{ message: string, type: string|null, devInsight: string|null, stack: string|null, codeContext: object|null } | null}
|
|
96
|
+
*/
|
|
97
|
+
this._errorInfo = null;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|
|
@@ -366,23 +381,118 @@ class Ammo {
|
|
|
366
381
|
// Per-call options: last arg can be { useLlm?, messageType? } when call is LLM-eligible (no explicit code).
|
|
367
382
|
const llmEligible =
|
|
368
383
|
args.length === 0 ||
|
|
369
|
-
(!isStatusCode(args[0]) &&
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
384
|
+
(!isStatusCode(args[0]) &&
|
|
385
|
+
!(
|
|
386
|
+
typeof args[0]?.statusCode === 'number' &&
|
|
387
|
+
typeof args[0]?.code === 'string'
|
|
388
|
+
));
|
|
389
|
+
let throwOpts =
|
|
390
|
+
/** @type {{ useLlm?: boolean, messageType?: 'endUser'|'developer' } | null} */ (
|
|
391
|
+
null
|
|
392
|
+
);
|
|
393
|
+
if (
|
|
394
|
+
llmEligible &&
|
|
395
|
+
args.length > 0 &&
|
|
396
|
+
isThrowOptions(args[args.length - 1])
|
|
397
|
+
) {
|
|
398
|
+
throwOpts =
|
|
399
|
+
/** @type {{ useLlm?: boolean, messageType?: 'endUser'|'developer' } } */ (
|
|
400
|
+
args.pop()
|
|
401
|
+
);
|
|
373
402
|
}
|
|
374
403
|
|
|
375
|
-
const useLlm =
|
|
376
|
-
llmEnabled &&
|
|
377
|
-
llmEligible &&
|
|
378
|
-
throwOpts?.useLlm !== false;
|
|
404
|
+
const useLlm = llmEnabled && llmEligible && throwOpts?.useLlm !== false;
|
|
379
405
|
|
|
380
406
|
if (useLlm) {
|
|
381
|
-
//
|
|
407
|
+
// Capture the stack string SYNCHRONOUSLY before any async work or fire() call,
|
|
408
|
+
// because the call stack unwinds as soon as we await or respond.
|
|
382
409
|
const stack =
|
|
383
|
-
args[0]
|
|
410
|
+
args[0] != null && typeof args[0].stack === 'string'
|
|
384
411
|
? args[0].stack
|
|
385
412
|
: new Error().stack;
|
|
413
|
+
const originalError =
|
|
414
|
+
args[0] !== undefined && args[0] !== null ? args[0] : undefined;
|
|
415
|
+
|
|
416
|
+
const { mode, channel, logFile } = getErrorsLlmConfig();
|
|
417
|
+
|
|
418
|
+
if (mode === 'async') {
|
|
419
|
+
// Respond immediately with a generic 500, then run LLM in the background.
|
|
420
|
+
this.fire(500, 'Internal Server Error');
|
|
421
|
+
|
|
422
|
+
// Stash basic error info synchronously so radar can read it on res.finish
|
|
423
|
+
// even before LLM completes. LLM result will update _errorInfo when ready.
|
|
424
|
+
const errorType =
|
|
425
|
+
originalError != null &&
|
|
426
|
+
typeof originalError.constructor?.name === 'string'
|
|
427
|
+
? originalError.constructor.name
|
|
428
|
+
: originalError !== undefined
|
|
429
|
+
? typeof originalError
|
|
430
|
+
: null;
|
|
431
|
+
this._errorInfo = {
|
|
432
|
+
message: 'Internal Server Error',
|
|
433
|
+
type: errorType,
|
|
434
|
+
devInsight: null,
|
|
435
|
+
stack: stack ?? null,
|
|
436
|
+
codeContext: null,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Fire-and-forget: capture context, call LLM, dispatch to channel.
|
|
440
|
+
const method = this.method;
|
|
441
|
+
const path = this.path;
|
|
442
|
+
const self = this;
|
|
443
|
+
captureCodeContext(stack)
|
|
444
|
+
.then((codeContext) => {
|
|
445
|
+
// Update _errorInfo with captured code context
|
|
446
|
+
if (self._errorInfo) self._errorInfo.codeContext = codeContext;
|
|
447
|
+
const context = {
|
|
448
|
+
codeContext,
|
|
449
|
+
method,
|
|
450
|
+
path,
|
|
451
|
+
// Always request devInsight in async mode — it goes to the channel, not the HTTP response.
|
|
452
|
+
includeDevInsight: true,
|
|
453
|
+
forceDevInsight: true,
|
|
454
|
+
...(throwOpts?.messageType && {
|
|
455
|
+
messageType: throwOpts.messageType,
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
if (originalError !== undefined) context.error = originalError;
|
|
459
|
+
return inferErrorFromContext(context).then((result) => ({
|
|
460
|
+
result,
|
|
461
|
+
codeContext,
|
|
462
|
+
}));
|
|
463
|
+
})
|
|
464
|
+
.then(({ result, codeContext }) => {
|
|
465
|
+
// Update _errorInfo with full LLM result
|
|
466
|
+
if (self._errorInfo) {
|
|
467
|
+
self._errorInfo.message = result.message;
|
|
468
|
+
self._errorInfo.devInsight = result.devInsight ?? null;
|
|
469
|
+
}
|
|
470
|
+
const channels = getChannels(channel, logFile);
|
|
471
|
+
const payload = buildPayload({
|
|
472
|
+
method,
|
|
473
|
+
path,
|
|
474
|
+
originalError,
|
|
475
|
+
codeContext,
|
|
476
|
+
statusCode: result.statusCode,
|
|
477
|
+
message: result.message,
|
|
478
|
+
devInsight: result.devInsight,
|
|
479
|
+
cached: result.cached,
|
|
480
|
+
rateLimited: result.rateLimited,
|
|
481
|
+
});
|
|
482
|
+
return dispatchToChannels(channels, payload);
|
|
483
|
+
})
|
|
484
|
+
.catch((err) => {
|
|
485
|
+
// Background LLM failed after HTTP response already sent — log the failure
|
|
486
|
+
// but do not attempt to respond again.
|
|
487
|
+
logger.warn(
|
|
488
|
+
`Background LLM dispatch failed: ${err?.message ?? err}`,
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Sync mode (default): block until LLM responds, then fire.
|
|
386
496
|
return captureCodeContext(stack)
|
|
387
497
|
.then((codeContext) => {
|
|
388
498
|
const context = {
|
|
@@ -390,23 +500,56 @@ class Ammo {
|
|
|
390
500
|
method: this.method,
|
|
391
501
|
path: this.path,
|
|
392
502
|
includeDevInsight: true,
|
|
393
|
-
...(throwOpts?.messageType && {
|
|
503
|
+
...(throwOpts?.messageType && {
|
|
504
|
+
messageType: throwOpts.messageType,
|
|
505
|
+
}),
|
|
394
506
|
};
|
|
395
|
-
if (
|
|
396
|
-
return inferErrorFromContext(context)
|
|
507
|
+
if (originalError !== undefined) context.error = originalError;
|
|
508
|
+
return inferErrorFromContext(context).then((result) => ({
|
|
509
|
+
result,
|
|
510
|
+
codeContext,
|
|
511
|
+
}));
|
|
397
512
|
})
|
|
398
|
-
.then(({
|
|
513
|
+
.then(({ result, codeContext }) => {
|
|
514
|
+
const { statusCode, message, devInsight } = result;
|
|
515
|
+
const errorType =
|
|
516
|
+
originalError != null &&
|
|
517
|
+
typeof originalError.constructor?.name === 'string'
|
|
518
|
+
? originalError.constructor.name
|
|
519
|
+
: originalError !== undefined
|
|
520
|
+
? typeof originalError
|
|
521
|
+
: null;
|
|
522
|
+
this._errorInfo = {
|
|
523
|
+
message,
|
|
524
|
+
type: errorType,
|
|
525
|
+
devInsight: devInsight ?? null,
|
|
526
|
+
stack: stack ?? null,
|
|
527
|
+
codeContext: codeContext ?? null,
|
|
528
|
+
};
|
|
399
529
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
400
530
|
const data =
|
|
401
531
|
!isProduction && devInsight
|
|
402
532
|
? { message, _dev: devInsight }
|
|
403
533
|
: message;
|
|
404
534
|
this.fire(statusCode, data);
|
|
535
|
+
})
|
|
536
|
+
.catch((err) => {
|
|
537
|
+
// LLM call failed (network error, timeout, etc.) — fall back to generic 500
|
|
538
|
+
// so the client always gets a response and we don't trigger an infinite retry loop.
|
|
539
|
+
logger.warn(`LLM error inference failed: ${err?.message ?? err}`);
|
|
540
|
+
this.fire(500, 'Internal Server Error');
|
|
405
541
|
});
|
|
406
542
|
}
|
|
407
543
|
|
|
408
544
|
// Sync path: explicit code/message or useLlm: false
|
|
409
545
|
if (args.length === 0) {
|
|
546
|
+
this._errorInfo = {
|
|
547
|
+
message: 'Internal Server Error',
|
|
548
|
+
type: null,
|
|
549
|
+
devInsight: null,
|
|
550
|
+
stack: null,
|
|
551
|
+
codeContext: null,
|
|
552
|
+
};
|
|
410
553
|
this.fire(500, 'Internal Server Error');
|
|
411
554
|
return;
|
|
412
555
|
}
|
|
@@ -414,29 +557,71 @@ class Ammo {
|
|
|
414
557
|
if (isStatusCode(args[0])) {
|
|
415
558
|
const statusCode = args[0];
|
|
416
559
|
const message = args[1] || toStatusMessage(statusCode);
|
|
560
|
+
this._errorInfo = {
|
|
561
|
+
message,
|
|
562
|
+
type: null,
|
|
563
|
+
devInsight: null,
|
|
564
|
+
stack: null,
|
|
565
|
+
codeContext: null,
|
|
566
|
+
};
|
|
417
567
|
this.fire(statusCode, message);
|
|
418
568
|
return;
|
|
419
569
|
}
|
|
420
570
|
|
|
421
|
-
if (
|
|
571
|
+
if (
|
|
572
|
+
typeof args[0]?.statusCode === 'number' &&
|
|
573
|
+
typeof args[0]?.code === 'string'
|
|
574
|
+
) {
|
|
422
575
|
const error = args[0];
|
|
423
|
-
this.
|
|
576
|
+
this._errorInfo = {
|
|
577
|
+
message: error.message,
|
|
578
|
+
type: error.constructor?.name ?? 'TejError',
|
|
579
|
+
devInsight: null,
|
|
580
|
+
stack: error.stack ?? null,
|
|
581
|
+
codeContext: null,
|
|
582
|
+
};
|
|
583
|
+
this.fire(error.statusCode, error.message);
|
|
424
584
|
return;
|
|
425
585
|
}
|
|
426
586
|
|
|
427
|
-
if (
|
|
587
|
+
if (
|
|
588
|
+
args[0] != null &&
|
|
589
|
+
typeof args[0].message === 'string' &&
|
|
590
|
+
typeof args[0].stack === 'string'
|
|
591
|
+
) {
|
|
428
592
|
const error = args[0];
|
|
429
593
|
if (!isNaN(parseInt(error.message))) {
|
|
430
594
|
const statusCode = parseInt(error.message);
|
|
431
595
|
const message = toStatusMessage(statusCode) || toStatusMessage(500);
|
|
596
|
+
this._errorInfo = {
|
|
597
|
+
message,
|
|
598
|
+
type: error.constructor.name,
|
|
599
|
+
devInsight: null,
|
|
600
|
+
stack: error.stack ?? null,
|
|
601
|
+
codeContext: null,
|
|
602
|
+
};
|
|
432
603
|
this.fire(statusCode, message);
|
|
433
604
|
return;
|
|
434
605
|
}
|
|
435
606
|
const statusCode = toStatusCode(error.message);
|
|
436
607
|
if (statusCode) {
|
|
608
|
+
this._errorInfo = {
|
|
609
|
+
message: error.message,
|
|
610
|
+
type: error.constructor.name,
|
|
611
|
+
devInsight: null,
|
|
612
|
+
stack: error.stack ?? null,
|
|
613
|
+
codeContext: null,
|
|
614
|
+
};
|
|
437
615
|
this.fire(statusCode, error.message);
|
|
438
616
|
return;
|
|
439
617
|
}
|
|
618
|
+
this._errorInfo = {
|
|
619
|
+
message: error.message,
|
|
620
|
+
type: error.constructor.name,
|
|
621
|
+
devInsight: null,
|
|
622
|
+
stack: error.stack ?? null,
|
|
623
|
+
codeContext: null,
|
|
624
|
+
};
|
|
440
625
|
this.fire(500, error.message);
|
|
441
626
|
return;
|
|
442
627
|
}
|
|
@@ -444,9 +629,23 @@ class Ammo {
|
|
|
444
629
|
const errorValue = args[0];
|
|
445
630
|
const statusCode = toStatusCode(errorValue);
|
|
446
631
|
if (statusCode) {
|
|
632
|
+
this._errorInfo = {
|
|
633
|
+
message: toStatusMessage(statusCode),
|
|
634
|
+
type: null,
|
|
635
|
+
devInsight: null,
|
|
636
|
+
stack: null,
|
|
637
|
+
codeContext: null,
|
|
638
|
+
};
|
|
447
639
|
this.fire(statusCode, toStatusMessage(statusCode));
|
|
448
640
|
return;
|
|
449
641
|
}
|
|
642
|
+
this._errorInfo = {
|
|
643
|
+
message: errorValue.toString(),
|
|
644
|
+
type: null,
|
|
645
|
+
devInsight: null,
|
|
646
|
+
stack: null,
|
|
647
|
+
codeContext: null,
|
|
648
|
+
};
|
|
450
649
|
this.fire(500, errorValue.toString());
|
|
451
650
|
}
|
|
452
651
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Request-scoped context using AsyncLocalStorage.
|
|
3
|
+
*
|
|
4
|
+
* Provides a safe, mutation-resistant store for per-request data.
|
|
5
|
+
* The store is initialized by the handler and is accessible from any
|
|
6
|
+
* code running within the same async execution context (middleware, handlers,
|
|
7
|
+
* service functions, etc.) without needing to thread parameters through.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // In a middleware or handler:
|
|
11
|
+
* import { getRequestId, getRequestStore } from '../server/context/request-context.js';
|
|
12
|
+
* const requestId = getRequestId(); // 'abc-123...' or 'no-context'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
/** @type {AsyncLocalStorage<{ requestId: string, startTime: number }>} */
|
|
19
|
+
export const requestContext = new AsyncLocalStorage();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the current request ID from async context, or 'no-context' if called
|
|
23
|
+
* outside of an active request (e.g., during startup).
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function getRequestId() {
|
|
27
|
+
return requestContext.getStore()?.requestId ?? 'no-context';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the full request store, or null if not in a request context.
|
|
32
|
+
* @returns {{ requestId: string, startTime: number } | undefined}
|
|
33
|
+
*/
|
|
34
|
+
export function getRequestStore() {
|
|
35
|
+
return requestContext.getStore();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Middleware that initializes the per-request AsyncLocalStorage store.
|
|
40
|
+
* Must be the first global middleware registered via app.midair().
|
|
41
|
+
*
|
|
42
|
+
* @param {import('../ammo.js').default} ammo
|
|
43
|
+
* @param {function(): Promise<void>} next
|
|
44
|
+
*/
|
|
45
|
+
export function contextMiddleware(ammo, next) {
|
|
46
|
+
const store = {
|
|
47
|
+
requestId: randomUUID(),
|
|
48
|
+
startTime: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
return requestContext.run(store, next);
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for request-context (AsyncLocalStorage).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
requestContext,
|
|
7
|
+
getRequestId,
|
|
8
|
+
getRequestStore,
|
|
9
|
+
contextMiddleware,
|
|
10
|
+
} from './request-context.js';
|
|
11
|
+
|
|
12
|
+
describe('requestContext', () => {
|
|
13
|
+
it('should return "no-context" when called outside a request', () => {
|
|
14
|
+
expect(getRequestId()).toBe('no-context');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return undefined store outside a request', () => {
|
|
18
|
+
expect(getRequestStore()).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should provide requestId within contextMiddleware', async () => {
|
|
22
|
+
const results = [];
|
|
23
|
+
const next = async () => {
|
|
24
|
+
results.push(getRequestId());
|
|
25
|
+
};
|
|
26
|
+
await contextMiddleware({}, next);
|
|
27
|
+
expect(results).toHaveLength(1);
|
|
28
|
+
expect(typeof results[0]).toBe('string');
|
|
29
|
+
expect(results[0]).toMatch(
|
|
30
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should provide startTime within contextMiddleware', async () => {
|
|
35
|
+
const before = Date.now();
|
|
36
|
+
let store;
|
|
37
|
+
await contextMiddleware({}, async () => {
|
|
38
|
+
store = getRequestStore();
|
|
39
|
+
});
|
|
40
|
+
expect(store.startTime).toBeGreaterThanOrEqual(before);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('each request should have unique requestId', async () => {
|
|
44
|
+
const ids = [];
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
await contextMiddleware({}, async () => {
|
|
47
|
+
ids.push(getRequestId());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const unique = new Set(ids);
|
|
51
|
+
expect(unique.size).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/server/endpoint.js
CHANGED
|
@@ -2,7 +2,16 @@ import isMiddlewareValid from './targets/middleware-validator.js';
|
|
|
2
2
|
import { isPathValid, standardizePath } from './targets/path-validator.js';
|
|
3
3
|
import isShootValid from './targets/shoot-validator.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Represents a single route endpoint: a path, handler, optional middlewares,
|
|
7
|
+
* allowed HTTP methods, and documentation metadata.
|
|
8
|
+
*
|
|
9
|
+
* Use the fluent builder methods to configure before registering with a Target.
|
|
10
|
+
*/
|
|
5
11
|
class Endpoint {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new Endpoint with empty defaults.
|
|
14
|
+
*/
|
|
6
15
|
constructor() {
|
|
7
16
|
this.path = '';
|
|
8
17
|
this.middlewares = [];
|
|
@@ -14,6 +23,12 @@ class Endpoint {
|
|
|
14
23
|
this.group = null;
|
|
15
24
|
}
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Set the full path by combining a base path and a path segment.
|
|
28
|
+
* @param {string} base - Base path (e.g. '/api')
|
|
29
|
+
* @param {string} path - Route path segment (e.g. '/users')
|
|
30
|
+
* @returns {Endpoint}
|
|
31
|
+
*/
|
|
17
32
|
setPath(base, path) {
|
|
18
33
|
const standardizedBase = standardizePath(base);
|
|
19
34
|
const standardizedPath = standardizePath(path);
|
package/server/error.js
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base error class for the Tejas framework.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework extend TejError. The constructor
|
|
5
|
+
* accepts the HTTP status code as the first argument (matching the existing
|
|
6
|
+
* call signature `new TejError(statusCode, message)`) and derives a
|
|
7
|
+
* machine-readable `code` string of the form `ERR_HTTP_<statusCode>`.
|
|
8
|
+
*
|
|
9
|
+
* Well-known codes are available as named constants on `TejError` for
|
|
10
|
+
* callers that want expressive error codes without hardcoding numbers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base framework error class.
|
|
15
|
+
*
|
|
16
|
+
* @extends {Error}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* throw new TejError(404, 'Not Found');
|
|
20
|
+
* // error.statusCode === 404
|
|
21
|
+
* // error.code === 'ERR_HTTP_404'
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // With cause chaining
|
|
25
|
+
* throw new TejError(500, 'Database failure', { cause: originalError });
|
|
26
|
+
*/
|
|
1
27
|
class TejError extends Error {
|
|
2
|
-
|
|
3
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {number} statusCode - HTTP status code (e.g. 404, 500)
|
|
30
|
+
* @param {string} message - Human-readable description
|
|
31
|
+
* @param {{ cause?: Error }} [options] - Optional native cause for chaining
|
|
32
|
+
*/
|
|
33
|
+
constructor(statusCode, message, options) {
|
|
34
|
+
super(message, options);
|
|
4
35
|
this.name = this.constructor.name;
|
|
5
|
-
|
|
36
|
+
/** @type {number} HTTP status code */
|
|
37
|
+
this.statusCode = statusCode;
|
|
38
|
+
/** @type {string} Machine-readable error code derived from status */
|
|
39
|
+
this.code = `ERR_HTTP_${statusCode}`;
|
|
40
|
+
Error.captureStackTrace(this, this.constructor);
|
|
6
41
|
}
|
|
7
42
|
}
|
|
8
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Named error codes for common framework scenarios.
|
|
46
|
+
* Use these instead of hardcoding numeric status codes at call sites.
|
|
47
|
+
*/
|
|
48
|
+
TejError.CODES = Object.freeze({
|
|
49
|
+
ERR_ROUTING_FAILED: 'ERR_ROUTING_FAILED',
|
|
50
|
+
ERR_INVALID_DEPENDENCY: 'ERR_INVALID_DEPENDENCY',
|
|
51
|
+
ERR_PLUGIN_LOAD_FAILED: 'ERR_PLUGIN_LOAD_FAILED',
|
|
52
|
+
ERR_CONFIG_INVALID: 'ERR_CONFIG_INVALID',
|
|
53
|
+
ERR_STREAM_OVERFLOW: 'ERR_STREAM_OVERFLOW',
|
|
54
|
+
ERR_AUTH_FAILED: 'ERR_AUTH_FAILED',
|
|
55
|
+
ERR_NOT_FOUND: 'ERR_HTTP_404',
|
|
56
|
+
ERR_METHOD_NOT_ALLOWED: 'ERR_HTTP_405',
|
|
57
|
+
ERR_UNAUTHORIZED: 'ERR_HTTP_401',
|
|
58
|
+
ERR_BAD_REQUEST: 'ERR_HTTP_400',
|
|
59
|
+
ERR_INTERNAL: 'ERR_HTTP_500',
|
|
60
|
+
});
|
|
61
|
+
|
|
9
62
|
export default TejError;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for TejError base class.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import TejError from './error.js';
|
|
6
|
+
|
|
7
|
+
describe('TejError', () => {
|
|
8
|
+
it('should set statusCode and derive ERR_HTTP_* code', () => {
|
|
9
|
+
const err = new TejError(404, 'Not Found');
|
|
10
|
+
expect(err.statusCode).toBe(404);
|
|
11
|
+
expect(err.code).toBe('ERR_HTTP_404');
|
|
12
|
+
expect(err.message).toBe('Not Found');
|
|
13
|
+
expect(err.name).toBe('TejError');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should capture a stack trace', () => {
|
|
17
|
+
const err = new TejError(500, 'Internal Server Error');
|
|
18
|
+
expect(typeof err.stack).toBe('string');
|
|
19
|
+
expect(err.stack).toContain('TejError');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should support native cause chaining', () => {
|
|
23
|
+
const cause = new Error('original');
|
|
24
|
+
const err = new TejError(500, 'Wrapped', { cause });
|
|
25
|
+
expect(err.cause).toBe(cause);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should extend Error', () => {
|
|
29
|
+
const err = new TejError(400, 'Bad Request');
|
|
30
|
+
expect(err).toBeInstanceOf(Error);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have frozen CODES constant', () => {
|
|
34
|
+
expect(Object.isFrozen(TejError.CODES)).toBe(true);
|
|
35
|
+
expect(TejError.CODES.ERR_ROUTING_FAILED).toBe('ERR_ROUTING_FAILED');
|
|
36
|
+
expect(TejError.CODES.ERR_NOT_FOUND).toBe('ERR_HTTP_404');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should derive correct codes for common statuses', () => {
|
|
40
|
+
expect(new TejError(400, '').code).toBe('ERR_HTTP_400');
|
|
41
|
+
expect(new TejError(401, '').code).toBe('ERR_HTTP_401');
|
|
42
|
+
expect(new TejError(403, '').code).toBe('ERR_HTTP_403');
|
|
43
|
+
expect(new TejError(500, '').code).toBe('ERR_HTTP_500');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for LLM error output channels.
|
|
3
|
+
* Subclasses implement dispatch() to send the LLM result wherever needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} ChannelPayload
|
|
8
|
+
* @property {string} timestamp - ISO 8601 timestamp of when the error occurred
|
|
9
|
+
* @property {string} method - HTTP method (e.g. GET, POST)
|
|
10
|
+
* @property {string} path - Request path
|
|
11
|
+
* @property {number} statusCode - LLM-inferred HTTP status code
|
|
12
|
+
* @property {string} message - LLM-inferred message
|
|
13
|
+
* @property {string} [devInsight] - Developer insight from LLM (always included in async mode)
|
|
14
|
+
* @property {{ type: string, message: string } | null} error - Original error summary
|
|
15
|
+
* @property {{ snippets: Array<{ file: string, line: number, snippet: string }> }} codeContext - Source context
|
|
16
|
+
* @property {boolean} [cached] - Whether this result came from the cache
|
|
17
|
+
* @property {boolean} [rateLimited] - Whether LLM was skipped due to rate limiting
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class ErrorChannel {
|
|
21
|
+
/**
|
|
22
|
+
* Dispatch an LLM error result to this channel.
|
|
23
|
+
* @param {ChannelPayload} payload
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
async dispatch(payload) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`dispatch() must be implemented by ${this.constructor.name}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|