@flancer32/teq-web 0.4.0 → 0.6.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 (46) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +236 -127
  3. package/ai/AGENTS.md +36 -0
  4. package/ai/abstractions.md +75 -0
  5. package/ai/examples/minimal-server.md +79 -0
  6. package/ai/overview.md +29 -0
  7. package/ai/rules.md +44 -0
  8. package/package.json +43 -37
  9. package/src/Back/Api/Handler.mjs +26 -0
  10. package/src/Back/Config/Runtime/Tls.mjs +89 -0
  11. package/src/Back/Config/Runtime.mjs +114 -0
  12. package/src/Back/Dto/Info.mjs +62 -0
  13. package/src/Back/Dto/RequestContext.mjs +33 -0
  14. package/src/Back/Dto/{Handler/Source.js → Source.mjs} +24 -25
  15. package/src/Back/Enum/Server/Type.mjs +13 -0
  16. package/src/Back/Enum/Stage.mjs +13 -0
  17. package/src/Back/Handler/Pre/Log.mjs +55 -0
  18. package/src/Back/Handler/Static/A/{Config.js → Config.mjs} +11 -4
  19. package/src/Back/Handler/Static/A/{Fallback.js → Fallback.mjs} +12 -4
  20. package/src/Back/Handler/Static/A/{FileService.js → FileService.mjs} +30 -17
  21. package/src/Back/Handler/Static/A/{Registry.js → Registry.mjs} +15 -7
  22. package/src/Back/Handler/Static/A/{Resolver.js → Resolver.mjs} +10 -3
  23. package/src/Back/Handler/Static.mjs +79 -0
  24. package/src/Back/Helper/Cast.mjs +116 -0
  25. package/src/Back/Helper/{Mime.js → Mime.mjs} +2 -0
  26. package/src/Back/Helper/Order/Kahn.mjs +69 -0
  27. package/src/Back/Helper/{Respond.js → Respond.mjs} +14 -7
  28. package/src/Back/Logger.mjs +57 -0
  29. package/src/Back/PipelineEngine.mjs +216 -0
  30. package/src/Back/{Server.js → Server.mjs} +38 -30
  31. package/types.d.ts +45 -22
  32. package/src/AGENTS.md +0 -108
  33. package/src/Back/Api/Handler.js +0 -26
  34. package/src/Back/Defaults.js +0 -6
  35. package/src/Back/Dispatcher.js +0 -115
  36. package/src/Back/Dto/Handler/Info.js +0 -68
  37. package/src/Back/Enum/Server/Type.js +0 -12
  38. package/src/Back/Enum/Stage.js +0 -10
  39. package/src/Back/Handler/Pre/Log.js +0 -45
  40. package/src/Back/Handler/Static.js +0 -63
  41. package/src/Back/Helper/Cast.js +0 -114
  42. package/src/Back/Helper/Order/Kahn.js +0 -66
  43. package/src/Back/Logger.js +0 -53
  44. package/src/Back/Server/Config/Tls.js +0 -55
  45. package/src/Back/Server/Config.js +0 -69
  46. package/teqfw.json +0 -8
@@ -1,23 +1,36 @@
1
+ // @ts-check
2
+
3
+ export const __deps__ = Object.freeze({
4
+ fs: 'node_fs',
5
+ http2: 'node_http2',
6
+ path: 'node_path',
7
+ logger: 'Fl32_Web_Back_Logger$',
8
+ helpMime: 'Fl32_Web_Back_Helper_Mime$',
9
+ resolver: 'Fl32_Web_Back_Handler_Static_A_Resolver$',
10
+ fallback: 'Fl32_Web_Back_Handler_Static_A_Fallback$',
11
+ });
12
+
1
13
  export default class Fl32_Web_Back_Handler_Static_A_FileService {
2
14
  /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
3
15
  /**
4
- * @param {typeof import('node:fs')} fs
5
- * @param {typeof import('node:http2')} http2
6
- * @param {typeof import('node:path')} path
7
- * @param {Fl32_Web_Back_Logger} logger
8
- * @param {Fl32_Web_Back_Helper_Mime} helpMime
9
- * @param {Fl32_Web_Back_Handler_Static_A_Resolver} resolver
10
- * @param {Fl32_Web_Back_Handler_Static_A_Fallback} fallback
16
+ * @param {object} params
17
+ * @param {Fl32_Web_Node_Fs} params.fs
18
+ * @param {Fl32_Web_Node_Http2} params.http2
19
+ * @param {Fl32_Web_Node_Path} params.path
20
+ * @param {Fl32_Web_Back_Logger} params.logger
21
+ * @param {Fl32_Web_Back_Helper_Mime} params.helpMime
22
+ * @param {Fl32_Web_Back_Handler_Static_A_Resolver} params.resolver
23
+ * @param {Fl32_Web_Back_Handler_Static_A_Fallback} params.fallback
11
24
  */
12
25
  constructor(
13
26
  {
14
- 'node:fs': fs,
15
- 'node:http2': http2,
16
- 'node:path': path,
17
- Fl32_Web_Back_Logger$: logger,
18
- Fl32_Web_Back_Helper_Mime$: helpMime,
19
- Fl32_Web_Back_Handler_Static_A_Resolver$: resolver,
20
- Fl32_Web_Back_Handler_Static_A_Fallback$: fallback,
27
+ fs,
28
+ http2,
29
+ path,
30
+ logger,
31
+ helpMime,
32
+ resolver,
33
+ fallback,
21
34
  }
22
35
  ) {
23
36
  /* eslint-enable jsdoc/check-param-names */
@@ -26,10 +39,10 @@ export default class Fl32_Web_Back_Handler_Static_A_FileService {
26
39
  /**
27
40
  * Serve a file for given config and relative path.
28
41
  *
29
- * @param {*} config
42
+ * @param {Fl32_Web_Back_Handler_Static_A_Config_Value} config
30
43
  * @param {string} rel
31
- * @param {*} req
32
- * @param {*} res
44
+ * @param {Fl32_Web_Node_Http_IncomingMessage|Fl32_Web_Node_Http2_ServerRequest} req
45
+ * @param {Fl32_Web_Back_Response_Target} res
33
46
  * @returns {Promise<boolean>} true if served
34
47
  */
35
48
  this.serve = async (config, rel, req, res) => {
@@ -1,24 +1,32 @@
1
+ // @ts-check
2
+
3
+ export const __deps__ = Object.freeze({
4
+ configFactory: 'Fl32_Web_Back_Handler_Static_A_Config$',
5
+ logger: 'Fl32_Web_Back_Logger$',
6
+ });
7
+
1
8
  export default class Fl32_Web_Back_Handler_Static_A_Registry {
2
9
  /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
3
10
  /**
4
- * @param {Fl32_Web_Back_Handler_Static_A_Config} configFactory
5
- * @param {Fl32_Web_Back_Logger} logger
11
+ * @param {object} params
12
+ * @param {Fl32_Web_Back_Handler_Static_A_Config} params.configFactory
13
+ * @param {Fl32_Web_Back_Logger} params.logger
6
14
  */
7
15
  constructor(
8
16
  {
9
- Fl32_Web_Back_Handler_Static_A_Config$: configFactory,
10
- Fl32_Web_Back_Logger$: logger,
17
+ configFactory,
18
+ logger,
11
19
  }
12
20
  ) {
13
21
  /* eslint-enable jsdoc/check-param-names */
14
- /** @type {Fl32_Web_Back_Dto_Handler_Source.Dto[]} */
22
+ /** @type {Fl32_Web_Back_Dto_Source[]} */
15
23
  let _configs = [];
16
24
 
17
25
  /**
18
26
  * Add configurations ensuring unique prefixes.
19
27
  * Existing entries are not modified.
20
28
  *
21
- * @param {Fl32_Web_Back_Dto_Handler_Source.Dto[]} dtoList
29
+ * @param {Fl32_Web_Back_Dto_Source[]} dtoList
22
30
  */
23
31
  this.addConfigs = function (dtoList = []) {
24
32
  const list = dtoList.map(dto => configFactory.create(dto));
@@ -37,7 +45,7 @@ export default class Fl32_Web_Back_Handler_Static_A_Registry {
37
45
  * Find configuration by matching URL prefix.
38
46
  *
39
47
  * @param {string} url
40
- * @returns {{config: *, rel: string}|null}
48
+ * @returns {Fl32_Web_Back_Handler_Static_A_Match|null}
41
49
  */
42
50
  this.find = function (url) {
43
51
  for (const cfg of _configs) {
@@ -1,17 +1,24 @@
1
+ // @ts-check
2
+
1
3
  /**
2
4
  * Enforces allow‐list rules and security checks when resolving
3
5
  * a relative URL to an absolute filesystem path under a given root.
4
6
  */
7
+ export const __deps__ = Object.freeze({
8
+ path: 'node_path',
9
+ });
10
+
5
11
  export default class Fl32_Web_Back_Handler_Static_A_Resolver {
6
12
  /**
7
- * @param {typeof import('node:path')} path
13
+ * @param {object} params
14
+ * @param {Fl32_Web_Node_Path} params.path
8
15
  */
9
- constructor({'node:path': path}) {
16
+ constructor({path}) {
10
17
  /**
11
18
  * Resolve a filesystem path for given config and relative URL part.
12
19
  * Applies allow rules and prevents path traversal.
13
20
  *
14
- * @param {{root: string, prefix: string, allow?: Record<string,string[]>}} config
21
+ * @param {Fl32_Web_Back_Handler_Static_A_Config_Value} config
15
22
  * @param {string} rel
16
23
  * @returns {string|null}
17
24
  * @throws {Error} On traversal or absolute rel paths.
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Universal static-file PROCESS handler.
5
+ *
6
+ * @implements Fl32_Web_Back_Api_Handler
7
+ */
8
+ export const __deps__ = Object.freeze({
9
+ registry: 'Fl32_Web_Back_Handler_Static_A_Registry$',
10
+ fileService: 'Fl32_Web_Back_Handler_Static_A_FileService$',
11
+ respond: 'Fl32_Web_Back_Helper_Respond$',
12
+ logger: 'Fl32_Web_Back_Logger$',
13
+ dtoInfoFactory: 'Fl32_Web_Back_Dto_Info__Factory$',
14
+ STAGE: 'Fl32_Web_Back_Enum_Stage$',
15
+ });
16
+
17
+ export default class Fl32_Web_Back_Handler_Static {
18
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
19
+ /**
20
+ * @param {object} params
21
+ * @param {Fl32_Web_Back_Handler_Static_A_Registry} params.registry
22
+ * @param {Fl32_Web_Back_Handler_Static_A_FileService} params.fileService
23
+ * @param {Fl32_Web_Back_Helper_Respond} params.respond
24
+ * @param {Fl32_Web_Back_Logger} params.logger
25
+ * @param {Fl32_Web_Back_Dto_Info$Factory} params.dtoInfoFactory
26
+ * @param {Fl32_Web_Back_Enum_Stage} params.STAGE
27
+ */
28
+ constructor(
29
+ {
30
+ registry,
31
+ fileService,
32
+ respond,
33
+ logger,
34
+ dtoInfoFactory,
35
+ STAGE,
36
+ }
37
+ ) {
38
+ /* eslint-enable jsdoc/check-param-names */
39
+
40
+ const _info = dtoInfoFactory.create({
41
+ name: this.constructor.name,
42
+ stage: STAGE.PROCESS,
43
+ });
44
+
45
+ /**
46
+ * Initialize registry with provided sources.
47
+ *
48
+ * @param {{sources: Fl32_Web_Back_Dto_Source[]}} params
49
+ * @returns {Promise<void>}
50
+ */
51
+ this.init = async ({sources = []} = {}) => {
52
+ registry.addConfigs(sources);
53
+ };
54
+
55
+ /**
56
+ * Attempt to handle incoming request.
57
+ *
58
+ * @param {Fl32_Web_Back_Dto_RequestContext} context
59
+ * @returns {Promise<void>}
60
+ */
61
+ this.handle = async (context) => {
62
+ const req = context.request;
63
+ const res = context.response;
64
+ if (!respond.isWritable(res)) return;
65
+ const urlPath = decodeURIComponent(req.url.split('?')[0]);
66
+ const match = registry.find(urlPath);
67
+ if (!match) return;
68
+ const served = await fileService.serve(match.config, match.rel, req, res);
69
+ if (served) {
70
+ context.complete();
71
+ }
72
+ };
73
+
74
+ /**
75
+ * @returns {Fl32_Web_Back_Dto_Info}
76
+ */
77
+ this.getRegistrationInfo = () => _info;
78
+ }
79
+ }
@@ -0,0 +1,116 @@
1
+ // @ts-check
2
+
3
+ export default class Fl32_Web_Back_Helper_Cast {
4
+ constructor() {
5
+ /**
6
+ * Cast input data into an array. Ensures the result is always an array.
7
+ * Optionally casts each item using the provided itemCast function.
8
+ *
9
+ * @param {*} data - Input data to be cast to array.
10
+ * @param {function(*): *} [itemCast] - Optional function to cast each item.
11
+ * @returns {Array}
12
+ */
13
+ this.array = function (data, itemCast) {
14
+ let arr = [];
15
+
16
+ if (Array.isArray(data)) {
17
+ arr = data;
18
+ } else if (data !== null) {
19
+ arr = [data];
20
+ }
21
+
22
+ return (typeof itemCast === 'function')
23
+ ? arr.map(itemCast).filter(v => v !== undefined)
24
+ : arr;
25
+ };
26
+
27
+ /**
28
+ * Cast input data into decimal 'number' data type.
29
+ * @param {*} data
30
+ * @returns {number|undefined}
31
+ */
32
+ this.decimal = function (data) {
33
+ const res = Number.parseFloat(data);
34
+ return ((typeof res === 'number') && (!isNaN(res))) ? res : undefined;
35
+ };
36
+
37
+ /**
38
+ * Cast input data into a valid enumeration value.
39
+ * Supports case normalization (upper/lower).
40
+ * If both `upper` and `lower` are true, `upper` takes precedence.
41
+ *
42
+ * @param {*} data - The input to cast.
43
+ * @param {object} enu - Object whose values represent valid enum values.
44
+ * @param {object} [params] - Parameters object.
45
+ * @param {boolean} [params.lower] - Normalize input to lower case before comparison.
46
+ * @param {boolean} [params.upper] - Normalize input to upper case before comparison.
47
+ * @returns {string|undefined}
48
+ */
49
+ this.enum = function (data, enu, {lower, upper} = {}) {
50
+ let norm = data;
51
+
52
+ if (typeof data === 'string') {
53
+ if (upper) norm = data.toUpperCase();
54
+ else if (lower) norm = data.toLowerCase();
55
+ }
56
+
57
+ const values = Object.values(enu);
58
+ return values.includes(norm) ? norm : undefined;
59
+ };
60
+
61
+ /**
62
+ * Cast input data into integer 'number' data type.
63
+ * @param {*} data - Input data to be cast to integer.
64
+ * @returns {number|undefined}
65
+ */
66
+ this.int = function (data) {
67
+ const norm = (typeof data === 'string') ? data.trim() : data;
68
+ const res = Number.parseInt(norm);
69
+ return ((typeof res === 'number') && (!isNaN(res))) ? res : undefined;
70
+ };
71
+
72
+ /**
73
+ * Cast input data into 'string' data type.
74
+ * @param {*} data - Input data to be cast to string.
75
+ * @returns {string|undefined}
76
+ */
77
+ this.string = function (data) {
78
+ if (typeof data === 'string') {
79
+ return data;
80
+ } else if (typeof data === 'number') {
81
+ return String(data);
82
+ } else if (typeof data === 'boolean') {
83
+ return (data) ? 'true' : 'false';
84
+ }
85
+ return undefined;
86
+ };
87
+
88
+ /**
89
+ * Cast an object to a map with string keys and array-of-string values.
90
+ * Throws error on invalid structure or values.
91
+ *
92
+ * @param {*} data - Raw input to cast.
93
+ * @returns {Record<string, string[]>}
94
+ */
95
+ this.stringArrayMap = function (data) {
96
+ if (data === undefined) return {};
97
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
98
+ throw new Error('Invalid value for allow');
99
+ }
100
+ const res = {};
101
+ for (const [key, arr] of Object.entries(data)) {
102
+ if (!Array.isArray(arr)) throw new Error(`Invalid allow list for ${key}`);
103
+ const k = this.string(key);
104
+ if (!k) throw new Error('Invalid allow key');
105
+ const items = [];
106
+ for (const item of arr) {
107
+ const val = this.string(item);
108
+ if (!val) throw new Error(`Invalid allow list for ${k}`);
109
+ items.push(val);
110
+ }
111
+ res[k] = items;
112
+ }
113
+ return res;
114
+ };
115
+ }
116
+ }
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  /**
2
4
  * MIME type helper with built-in mapping for common file extensions.
3
5
  * Can be replaced or extended by the application via the DI container.
@@ -0,0 +1,69 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Sorts named handlers by relative `before` / `after` constraints using Kahn's algorithm.
5
+ */
6
+ export default class Fl32_Web_Back_Helper_Order_Kahn {
7
+ constructor() {
8
+ /**
9
+ * Topologically sorts handlers with `name`, `before`, `after` fields.
10
+ *
11
+ * @param {Fl32_Web_Back_Api_Handler[]} handlers - Handlers to sort.
12
+ * @returns {Fl32_Web_Back_Api_Handler[]} - Sorted list.
13
+ * @throws {Error} - If circular dependency is detected.
14
+ */
15
+ this.sort = function (handlers) {
16
+ const nameToHandler = new Map();
17
+ const graph = new Map(); // name => Set of downstream nodes
18
+ const inDegree = new Map(); // name => number of incoming edges
19
+
20
+ // Initialize maps
21
+ for (const h of handlers) {
22
+ const info = h.getRegistrationInfo();
23
+ const name = info.name;
24
+ nameToHandler.set(name, h);
25
+ graph.set(name, new Set());
26
+ inDegree.set(name, 0);
27
+ }
28
+
29
+ // Build graph
30
+ for (const h of handlers) {
31
+ const {name, after = [], before = []} = h.getRegistrationInfo();
32
+
33
+ for (const dep of after) {
34
+ if (!graph.has(dep)) continue;
35
+ graph.get(dep).add(name);
36
+ inDegree.set(name, inDegree.get(name) + 1);
37
+ }
38
+
39
+ for (const dep of before) {
40
+ if (!graph.has(dep)) continue;
41
+ graph.get(name).add(dep);
42
+ inDegree.set(dep, inDegree.get(dep) + 1);
43
+ }
44
+ }
45
+
46
+ // Kahn's algorithm
47
+ const queue = [];
48
+ for (const [name, count] of inDegree.entries()) {
49
+ if (count === 0) queue.push(name);
50
+ }
51
+
52
+ const sorted = [];
53
+ while (queue.length > 0) {
54
+ const name = queue.shift();
55
+ sorted.push(nameToHandler.get(name));
56
+ for (const neighbor of graph.get(name)) {
57
+ inDegree.set(neighbor, inDegree.get(neighbor) - 1);
58
+ if (inDegree.get(neighbor) === 0) queue.push(neighbor);
59
+ }
60
+ }
61
+
62
+ if (sorted.length !== handlers.length) {
63
+ throw new Error('Circular dependency detected among handlers');
64
+ }
65
+
66
+ return sorted;
67
+ };
68
+ }
69
+ }
@@ -1,11 +1,18 @@
1
+ // @ts-check
2
+
1
3
  /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
4
+ export const __deps__ = Object.freeze({
5
+ http2: 'node_http2',
6
+ });
7
+
2
8
  export default class Fl32_Web_Back_Helper_Respond {
3
9
  /**
4
- * @param {typeof import('node:http2')} http2
10
+ * @param {object} params
11
+ * @param {Fl32_Web_Node_Http2} params.http2
5
12
  */
6
13
  constructor(
7
14
  {
8
- 'node:http2': http2,
15
+ http2,
9
16
  }
10
17
  ) {
11
18
  // VARS
@@ -36,9 +43,9 @@ export default class Fl32_Web_Back_Helper_Respond {
36
43
  * Sends an HTTP response with a given status code.
37
44
  *
38
45
  * @param {object} params
39
- * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} params.res - HTTP response object.
40
- * @param {{[key: string]: string}} [params.headers={}] - Custom headers.
41
- * @param {string|object} [params.body=''] - Response body.
46
+ * @param {Fl32_Web_Back_Response_Target} params.res - HTTP response object.
47
+ * @param {Fl32_Web_Back_Response_Headers} [params.headers={}] - Custom headers.
48
+ * @param {Fl32_Web_Back_Response_Body} [params.body=''] - Response body.
42
49
  * @param {number} status - HTTP status code.
43
50
  * @returns {boolean} - `true` if response was sent, `false` if headers were already sent.
44
51
  */
@@ -145,11 +152,11 @@ export default class Fl32_Web_Back_Helper_Respond {
145
152
 
146
153
  /**
147
154
  * Checks if the response is writable and not yet sent.
148
- * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
155
+ * @param {Fl32_Web_Back_Response_Target} res
149
156
  * @returns {boolean}
150
157
  */
151
158
  this.isWritable = function (res) {
152
159
  return !res.headersSent && !res.writableEnded;
153
160
  };
154
161
  }
155
- }
162
+ }
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Simple logger implementation that delegates to the native console.
5
+ */
6
+ export default class Fl32_Web_Back_Logger {
7
+ constructor() {
8
+ /**
9
+ * Logs an error message.
10
+ * @param {...any} args - The error message or data.
11
+ */
12
+ this.error = function (...args) {
13
+ console.error('[ERROR]', ...args);
14
+ };
15
+
16
+ /**
17
+ * Logs a warning message.
18
+ * @param {...any} args - The warning message or data.
19
+ */
20
+ this.warn = function (...args) {
21
+ console.warn('[WARN]', ...args);
22
+ };
23
+
24
+ /**
25
+ * Logs an informational message.
26
+ * @param {...any} args - The informational message or data.
27
+ */
28
+ this.info = function (...args) {
29
+ console.info('[INFO]', ...args);
30
+ };
31
+
32
+ /**
33
+ * Logs a debug message.
34
+ * @param {...any} args - The debug message or data.
35
+ */
36
+ this.debug = function (...args) {
37
+ console.debug('[DEBUG]', ...args);
38
+ };
39
+
40
+ /**
41
+ * Logs a trace message.
42
+ * @param {...any} args - The trace message or data.
43
+ */
44
+ this.trace = function (...args) {
45
+ console.trace('[TRACE]', ...args);
46
+ };
47
+
48
+ /**
49
+ * Logs an exception with optional additional context.
50
+ * @param {Error} exception - The exception to log.
51
+ * @param {...any} context - Additional context or metadata.
52
+ */
53
+ this.exception = function (exception, ...context) {
54
+ console.error('[EXCEPTION]', exception.stack || exception.toString(), ...context);
55
+ };
56
+ }
57
+ }