@flancer32/teq-web 0.2.0 → 0.3.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/AGENTS.md ADDED
@@ -0,0 +1,101 @@
1
+ # AGENTS.md
2
+
3
+ ## Project: @flancer32/teq-web
4
+
5
+ This project implements a request dispatcher plugin for Tequila Framework (TeqFW).
6
+ The plugin provides a multi-stage handler system (`pre`, `process`, `post`) and integrates directly with Node.js servers (`http`, `http2`, `https`) without external dependencies.
7
+
8
+ ---
9
+
10
+ ## Project Structure
11
+
12
+ | Directory | Description |
13
+ |----------------------------|----------------------------------------------------------------------------|
14
+ | `/src/Back/Api/Handler.js` | Interface for all request handlers. |
15
+ | `/src/Back/Dispatcher.js` | Core dispatcher that orchestrates handler execution. |
16
+ | `/src/Back/Handler/` | Built-in request handlers (`Pre_Log`, `Static`, `Source`, etc). |
17
+ | `/src/Back/Helper/` | Internal helpers (`Mime`, `Respond`, `Order_Kahn`, etc). |
18
+ | `/src/Back/Dto/` | DTO factories used to pass typed configuration and metadata. |
19
+ | `/src/Back/Server.js` | Standalone HTTP(S) server implementation using built-in Node.js libraries. |
20
+
21
+ ---
22
+
23
+ ## Execution Agents
24
+
25
+ ### Dispatcher
26
+
27
+ - File: `Back/Dispatcher.js`
28
+ - Role: Core runtime coordinator for HTTP requests.
29
+ - Interface: uses `Fl32_Web_Back_Api_Handler` and topological ordering via `Order_Kahn`.
30
+
31
+ ### Server
32
+
33
+ - File: `Back/Server.js`
34
+ - Role: HTTP/1.1, HTTP/2, or HTTPS web server.
35
+ - Launches `Dispatcher.onEventRequest()` on each request.
36
+
37
+ ### Handlers
38
+
39
+ - Files: `Back/Handler/Pre_Log.js`, `.../Static.js`, `.../Source.js`
40
+ - Role: Modular request processors, registered via `Dispatcher.addHandler()`.
41
+ - Lifecycle: Initialized once, executed per request by dispatcher.
42
+
43
+ ---
44
+
45
+ ## Code Style
46
+
47
+ - Language: Modern JavaScript (ES2022+).
48
+ - No static imports (uses DI-based module resolution).
49
+ - All comments and messages must be in English (strict rule).
50
+ - File naming: PascalCase with dot-separated exports, e.g. `Fl32_Web_Back_Enum_Stage`.
51
+
52
+ ---
53
+
54
+ ## Testing
55
+
56
+ - Unit tests must be provided for all runtime agents (Dispatcher, Handlers).
57
+ - Test framework: `node:test`.
58
+ - Mocks: registered via custom `buildTestContainer()` helper.
59
+ - Location: colocated or `/test/` folder depending on iteration.
60
+
61
+ ---
62
+
63
+ ## Build & Execution
64
+
65
+ - This project is a TeqFW plugin. It is not compiled or bundled.
66
+ - Executed in-place by Tequila runtime.
67
+ - Requires a DI container to resolve dependencies at runtime.
68
+
69
+ ---
70
+
71
+ ## Contribution Rules (for AI Agents)
72
+
73
+ - When creating new Handlers, they **must** implement `Fl32_Web_Back_Api_Handler` and provide `getRegistrationInfo()`.
74
+ - Handlers must be registered with `Dispatcher.addHandler()` and ordered with `orderHandlers()`.
75
+ - All request-handling code **must** call `respond.isWritable(res)` before sending a response.
76
+ - Do not modify existing dispatcher logic directly — create a new handler or helper instead.
77
+
78
+ ---
79
+
80
+ ## Conventions for AI Tools (Codex, etc.)
81
+
82
+ - Start by inspecting `Dispatcher.js` and `Handler/` folder to locate runtime behavior.
83
+ - DTOs are created via `*.create(data)` methods and used to validate configs.
84
+ - Use `getRegistrationInfo().stage` to determine when a handler runs (`pre`, `process`, `post`).
85
+ - Prefer composition over inheritance. Avoid using `extends`.
86
+ - Respect topological order defined via `before`/`after`.
87
+
88
+ ---
89
+
90
+ ## CI/Automation
91
+
92
+ - No CI defined yet. Future CI will:
93
+ - Validate handler registration structure.
94
+ - Enforce code style and comment policy.
95
+ - Run full test suite before merge.
96
+
97
+ ---
98
+
99
+ ## License
100
+
101
+ This project is licensed under the Apache-2.0 license.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2025-08-21
4
+
5
+ ### Added
6
+ - Added TeqFW descriptor to define package namespace for @teqfw/core.
7
+
8
+ ## [0.3.0] - 2025-06-26
9
+
10
+ ### Added
11
+ - Generalized NPM handler into a Source handler with DTO-based configuration.
12
+ - Unit tests for the dispatcher and built-in handlers.
13
+
14
+ ### Changed
15
+ - Static handler refactored into modular components with before/after ordering.
16
+
17
+ ### Fixed
18
+ - Improved validation messages for static handler configuration.
19
+ - File service now reports specific filesystem errors.
20
+
3
21
  ## [0.2.0] - 2025-06-21
4
22
 
5
23
  ### Added
6
- - Integration test covering NPM handler with the built-in server.
7
- - Built-in NPM handler for serving files from `node_modules`.
8
- - JSDoc examples for NPM handler initialization.
24
+ - Integration test covering static file serving from `node_modules`.
25
+ - Static handler can serve files from `node_modules` via `Handler_Source`.
26
+ - JSDoc examples for initializing the static handler with a `Handler_Source` DTO.
9
27
 
10
28
  ## [0.1.0] - 2025-06-11
11
29
 
package/README.md CHANGED
@@ -62,6 +62,11 @@ This example shows how to create a minimal application that:
62
62
  * Registers two handlers: a logger and a static file server
63
63
  * Starts a secure HTTPS server using built-in components
64
64
 
65
+ The static handler reads from one or more **sources** described by the
66
+ `Handler_Source` DTO. To expose selected files from `node_modules`, create a
67
+ source with `root: 'node_modules'` and pass it to `Handler_Static` during
68
+ initialization.
69
+
65
70
  ```js
66
71
  import Container from '@teqfw/di';
67
72
  import {readFileSync} from 'node:fs';
@@ -81,20 +86,22 @@ resolver.addNamespaceRoot('Fl32_Web_', './node_modules/@flancer32/teq-web/src');
81
86
 
82
87
  // Get and configure built-in handlers
83
88
  const logHandler = await container.get('Fl32_Web_Back_Handler_Pre_Log$');
84
- const npmHandler = await container.get('Fl32_Web_Back_Handler_Npm$');
85
89
  const staticHandler = await container.get('Fl32_Web_Back_Handler_Static$');
86
- await npmHandler.init({
90
+ const SourceCfg = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
91
+ const srcNpm = SourceCfg.create({
92
+ root: 'node_modules',
93
+ prefix: '/node_modules/',
87
94
  allow: {
88
95
  vue: ['dist/vue.global.prod.js'],
89
96
  '@teqfw/di': ['src/Container.js'],
90
97
  }
91
98
  });
92
- await staticHandler.init({rootPath: webRoot});
99
+ const srcWeb = SourceCfg.create({ root: webRoot, prefix: '/' });
100
+ await staticHandler.init({sources: [srcNpm, srcWeb]});
93
101
 
94
102
  // Register handlers
95
103
  const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
96
104
  dispatcher.addHandler(logHandler);
97
- dispatcher.addHandler(npmHandler);
98
105
  dispatcher.addHandler(staticHandler);
99
106
 
100
107
  // Create and start the server
@@ -113,8 +120,7 @@ await server.start({
113
120
  This will start an HTTPS server on port `3443` with:
114
121
 
115
122
  * `Fl32_Web_Back_Handler_Pre_Log` logging each request method and URL;
116
- * `Fl32_Web_Back_Handler_Npm` serving allowed files from `node_modules`;
117
- * `Fl32_Web_Back_Handler_Static` serving files from the `/web` folder.
123
+ * `Fl32_Web_Back_Handler_Static` serving files from `node_modules` and the `/web` folder.
118
124
 
119
125
  ---
120
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flancer32/teq-web",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Node.js web plugin supporting Express and Fastify integration, with built-in server for standalone operation",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/AGENTS.md ADDED
@@ -0,0 +1,108 @@
1
+ # AI Agents Configuration for Source Directory
2
+
3
+ This document defines **long-term, project-agnostic** conventions that AI agents must follow when reading, creating, refactoring, or testing code under any `src/` directory. These guidelines are **permanent** and apply to **all** tasks in this area.
4
+
5
+ ---
6
+
7
+ ## 1. Directory & File Organization
8
+
9
+ * **AZ-structuring**
10
+
11
+ * **A-struct**: decompose a single “root” module into its private implementation parts under an `A/` subfolder.
12
+
13
+ ```text
14
+ src/Feature/Component.js
15
+ src/Feature/Component/A/Part1.js
16
+ src/Feature/Component/A/Part2.js
17
+ ```
18
+
19
+ Files under `A/` are private to their parent.
20
+ * **Z-struct**: helper modules whose changes do not affect siblings live under a `Z/` subfolder.
21
+
22
+ * **Visibility boundaries**: anything **outside** `A/` or `Z/` is public API.
23
+
24
+ * **Role → Feature layering**
25
+
26
+ * First level: by architectural layer (e.g. `Back/`, `Front/`, `Shared/`).
27
+ * Second level: by business feature or component.
28
+
29
+ * **Mirror in tests**
30
+
31
+ * For every `src/.../X.js` there must be a matching `test/unit/.../X.test.mjs` with the same relative path and filename.
32
+
33
+ ---
34
+
35
+ ## 2. Class Naming (FQN)
36
+
37
+ * Map file path to fully qualified class name (FQN). Example:
38
+
39
+ ```text
40
+ src/Back/Handler/Static/A/Config.js => class Back_Handler_Static_A_Config
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 3. Dependency Injection, `this` & Closures
46
+
47
+ * **DI Container**
48
+
49
+ * Inject all external services (file system, path, network, helpers, loggers, factories, enums) via a DI container.
50
+ * **Private state via closure**
51
+
52
+ * Do **not** assign injected dependencies to `this`.
53
+ * Capture each in a `const` inside the constructor and expose only public methods.
54
+
55
+ ```js
56
+ class Example {
57
+ constructor({helperService, logger}) {
58
+ this.handle = (req, res) => {
59
+ if (!helperService.isWritable(res)) return false;
60
+ logger.info('Processing');
61
+ return true;
62
+ };
63
+ }
64
+ }
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 4. Comments & Annotations
70
+
71
+ * **Language**: all comments and JSDoc must be in **English**.
72
+ * **JSDoc**: use `@param {Type}` and `@returns {Type}` on every public method.
73
+ * **Class annotation**: annotate all classes with a JSDoc `@class` tag and description.
74
+
75
+ ---
76
+
77
+ ## 5. Error Handling
78
+
79
+ * **Module-level**
80
+
81
+ * Do **not** catch all exceptions in each module.
82
+ * Catch only **expected, domain-specific** errors when a meaningful fallback exists.
83
+ * **Top-level handler**
84
+
85
+ * Wrap the entire pipeline in a single `try/catch` at the entry point.
86
+ * In that catch, use `logger.exception(error)` and return a safe default (`false`, `null`, or `[]`).
87
+
88
+ ---
89
+
90
+ ## 6. Testing Conventions
91
+
92
+ Refer to the [AI Agent Unit Testing Instructions](../test/unit/AGENTS.md) for project-specific testing guidelines.
93
+
94
+ ---
95
+
96
+ ## 7. Ongoing Responsibilities
97
+
98
+ AI agents must always:
99
+
100
+ 1. **Enforce AZ-structuring** when adding or refactoring code.
101
+ 2. **Preserve FQN mapping** between file paths and class names.
102
+ 3. **Use DI + closures**—never attach injected services to `this`.
103
+ 4. **Write English JSDoc** for every new or modified public API, including `@class` on all classes.
104
+ 5. **Catch and log errors** at the top level; handle known cases locally.
105
+ 6. **Generate or update unit tests** with full coverage for every behavior change (see unit testing guidelines).
106
+ 7. **Maintain test-source mirror** so no source file exists without a corresponding test.
107
+
108
+ These policies are **permanent** and must be validated on every code change.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Factory for file source configuration DTO used by Static handler.
3
+ */
4
+ export default class Fl32_Web_Back_Dto_Handler_Source {
5
+ /* eslint-disable jsdoc/require-param-description */
6
+ /**
7
+ * @param {Fl32_Web_Back_Helper_Cast} cast
8
+ */
9
+ constructor(
10
+ {
11
+ Fl32_Web_Back_Helper_Cast$: cast,
12
+ }
13
+ ) {
14
+ /* eslint-enable jsdoc/require-param-description */
15
+ /**
16
+ * Create validated DTO for source configuration.
17
+ *
18
+ * @param {*} [data]
19
+ * @returns {Dto}
20
+ */
21
+ this.create = function (data) {
22
+ const res = new Dto();
23
+ if (data) {
24
+ res.root = cast.string(data.root);
25
+ res.prefix = cast.string(data.prefix);
26
+ res.allow = cast.stringArrayMap(data.allow);
27
+ res.defaults = cast.array(data.defaults, cast.string);
28
+ }
29
+ return res;
30
+ };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @memberOf Fl32_Web_Back_Dto_Handler_Source
36
+ */
37
+ class Dto {
38
+ /** @type {string} */
39
+ root;
40
+ /** @type {string} */
41
+ prefix;
42
+ /** @type {{[key: string]: string[]}} */
43
+ allow = {};
44
+ /** @type {string[]} */
45
+ defaults = [];
46
+ }
@@ -0,0 +1,58 @@
1
+ export default class Fl32_Web_Back_Handler_Static_A_Config {
2
+ static DEFAULT_FILES = ['index.html', 'index.htm', 'index.txt'];
3
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
4
+ /**
5
+ * @param {typeof import('node:path')} path
6
+ */
7
+ constructor(
8
+ {
9
+ 'node:path': path,
10
+ }
11
+ ) {
12
+ /* eslint-enable jsdoc/check-param-names */
13
+
14
+ /**
15
+ * Normalize DTO fields into configuration object.
16
+ *
17
+ * @param {Fl32_Web_Back_Dto_Handler_Source.Dto} dto
18
+ * @returns {{root:string,prefix:string,allow?:Record<string,string[]>,defaults:string[]}}
19
+ * @throws {Error} When required fields are missing or invalid.
20
+ */
21
+ this.create = (dto) => {
22
+ if (!dto || typeof dto.root !== 'string') {
23
+ throw new Error("Field 'root' must be a string");
24
+ }
25
+ let prefix = dto.prefix ?? '/';
26
+ if (typeof prefix !== 'string') {
27
+ throw new Error("Field 'prefix' must be a string");
28
+ }
29
+ if (!prefix.endsWith('/')) prefix += '/';
30
+
31
+ const root = path.resolve(dto.root);
32
+
33
+ let allow;
34
+ if (dto.allow !== undefined) {
35
+ if (typeof dto.allow !== 'object' || dto.allow === null || Array.isArray(dto.allow)) {
36
+ throw new Error("Field 'allow' must be an object");
37
+ }
38
+ for (const [k, arr] of Object.entries(dto.allow)) {
39
+ if (!Array.isArray(arr) || arr.some(v => typeof v !== 'string')) {
40
+ throw new Error(`Field 'allow.${k}' must be an array of strings`);
41
+ }
42
+ }
43
+ allow = dto.allow;
44
+ }
45
+
46
+ let defaults = dto.defaults;
47
+ if (defaults !== undefined && defaults.length) {
48
+ if (!Array.isArray(defaults) || defaults.some(v => typeof v !== 'string')) {
49
+ throw new Error("Field 'defaults' must be an array of strings");
50
+ }
51
+ } else {
52
+ defaults = Fl32_Web_Back_Handler_Static_A_Config.DEFAULT_FILES;
53
+ }
54
+
55
+ return {root, prefix, allow, defaults};
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,39 @@
1
+ export default class Fl32_Web_Back_Handler_Static_A_Fallback {
2
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
3
+ /**
4
+ * @param {typeof import('node:fs')} fs
5
+ * @param {typeof import('node:path')} path
6
+ */
7
+ constructor(
8
+ {
9
+ 'node:fs': fs,
10
+ 'node:path': path,
11
+ }
12
+ ) {
13
+ /* eslint-enable jsdoc/check-param-names */
14
+
15
+ /**
16
+ * Apply default index fallback for directories.
17
+ *
18
+ * @param {string} fsPath
19
+ * @param {string[]} defaults
20
+ * @returns {Promise<string|null>} Path to existing file or null.
21
+ */
22
+ this.apply = async (fsPath, defaults) => {
23
+ let stat;
24
+ try { stat = await fs.promises.stat(fsPath); } catch { return null; }
25
+
26
+ if (stat.isDirectory()) {
27
+ for (const file of defaults) {
28
+ const candidate = path.join(fsPath, file);
29
+ try {
30
+ const s = await fs.promises.stat(candidate);
31
+ if (s.isFile()) return candidate;
32
+ } catch { /* ignore */ }
33
+ }
34
+ return null;
35
+ }
36
+ return stat.isFile() ? fsPath : null;
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,69 @@
1
+ export default class Fl32_Web_Back_Handler_Static_A_FileService {
2
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
3
+ /**
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
11
+ */
12
+ constructor(
13
+ {
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,
21
+ }
22
+ ) {
23
+ /* eslint-enable jsdoc/check-param-names */
24
+ const {constants: H2} = http2;
25
+
26
+ /**
27
+ * Serve a file for given config and relative path.
28
+ *
29
+ * @param {*} config
30
+ * @param {string} rel
31
+ * @param {*} req
32
+ * @param {*} res
33
+ * @returns {Promise<boolean>} true if served
34
+ */
35
+ this.serve = async (config, rel, req, res) => {
36
+ let fsPath;
37
+ try {
38
+ fsPath = resolver.resolve(config, rel);
39
+ if (!fsPath) return false;
40
+
41
+ fsPath = await fallback.apply(fsPath, config.defaults);
42
+ if (!fsPath) return false;
43
+
44
+ const stat = await fs.promises.stat(fsPath);
45
+ if (!stat.isFile()) return false;
46
+
47
+ const stream = fs.createReadStream(fsPath);
48
+ const ext = path.extname(fsPath).toLowerCase();
49
+ const headers = {
50
+ [H2.HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
51
+ [H2.HTTP2_HEADER_CONTENT_TYPE]: helpMime.getByExt(ext),
52
+ [H2.HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString(),
53
+ };
54
+ res.writeHead(H2.HTTP_STATUS_OK, headers);
55
+ stream.pipe(res);
56
+ return true;
57
+ } catch (e) {
58
+ if (e?.code === 'ENOENT') {
59
+ logger.info(`File not found: ${fsPath}`);
60
+ } else if (e?.code === 'EACCES' || e?.code === 'EPERM') {
61
+ logger.warn(`Access denied: ${fsPath}`);
62
+ } else {
63
+ logger.exception(e);
64
+ }
65
+ return false;
66
+ }
67
+ };
68
+ }
69
+ }
@@ -0,0 +1,52 @@
1
+ export default class Fl32_Web_Back_Handler_Static_A_Registry {
2
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
3
+ /**
4
+ * @param {Fl32_Web_Back_Handler_Static_A_Config} configFactory
5
+ * @param {Fl32_Web_Back_Logger} logger
6
+ */
7
+ constructor(
8
+ {
9
+ Fl32_Web_Back_Handler_Static_A_Config$: configFactory,
10
+ Fl32_Web_Back_Logger$: logger,
11
+ }
12
+ ) {
13
+ /* eslint-enable jsdoc/check-param-names */
14
+ /** @type {Fl32_Web_Back_Dto_Handler_Source.Dto[]} */
15
+ let _configs = [];
16
+
17
+ /**
18
+ * Add configurations ensuring unique prefixes.
19
+ * Existing entries are not modified.
20
+ *
21
+ * @param {Fl32_Web_Back_Dto_Handler_Source.Dto[]} dtoList
22
+ */
23
+ this.addConfigs = function (dtoList = []) {
24
+ const list = dtoList.map(dto => configFactory.create(dto));
25
+ for (const cfg of list) {
26
+ if (!_configs.some(c => c.prefix === cfg.prefix)) {
27
+ _configs.push(cfg);
28
+ } else {
29
+ logger.warn(`Static config with prefix ${cfg.prefix} already exists`);
30
+ }
31
+ }
32
+ _configs.sort((a, b) => b.prefix.length - a.prefix.length);
33
+ };
34
+
35
+
36
+ /**
37
+ * Find configuration by matching URL prefix.
38
+ *
39
+ * @param {string} url
40
+ * @returns {{config: *, rel: string}|null}
41
+ */
42
+ this.find = function (url) {
43
+ for (const cfg of _configs) {
44
+ if (url.startsWith(cfg.prefix)) {
45
+ const rel = url.slice(cfg.prefix.length);
46
+ return {config: cfg, rel};
47
+ }
48
+ }
49
+ return null;
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Enforces allow‐list rules and security checks when resolving
3
+ * a relative URL to an absolute filesystem path under a given root.
4
+ */
5
+ export default class Fl32_Web_Back_Handler_Static_A_Resolver {
6
+ /**
7
+ * @param {typeof import('node:path')} path
8
+ */
9
+ constructor({'node:path': path}) {
10
+ /**
11
+ * Resolve a filesystem path for given config and relative URL part.
12
+ * Applies allow rules and prevents path traversal.
13
+ *
14
+ * @param {{root: string, prefix: string, allow?: Record<string,string[]>}} config
15
+ * @param {string} rel
16
+ * @returns {string|null}
17
+ * @throws {Error} On traversal or absolute rel paths.
18
+ */
19
+ this.resolve = (config, rel) => {
20
+ // disallow path‐traversal or absolute rel
21
+ if (rel.includes('..') || path.isAbsolute(rel)) {
22
+ throw new Error('Static access denied');
23
+ }
24
+
25
+ let pkgName;
26
+ let subPath = '';
27
+
28
+ if (!config.allow) {
29
+ let fsPath = path.resolve(config.root, rel);
30
+ if (/[\\/]$/.test(fsPath) && fsPath.length > 1) {
31
+ fsPath = fsPath.replace(/[\\/]+$/, '');
32
+ }
33
+ if (!fsPath.startsWith(config.root)) {
34
+ throw new Error('Resolved path is outside the root');
35
+ }
36
+ return fsPath;
37
+ }
38
+
39
+ // root‐level allow: '.' rules permit any rel under root
40
+ if (config.allow?.['.'] != null) {
41
+ pkgName = '.';
42
+ subPath = rel;
43
+ } else if (config.allow) {
44
+ for (const key of Object.keys(config.allow)) {
45
+ if (rel === key || rel.startsWith(`${key}/`)) {
46
+ pkgName = key;
47
+ const offset = key.length + (rel[key.length] === '/' ? 1 : 0);
48
+ subPath = rel.slice(offset);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+
54
+ if (!pkgName) {
55
+ return null;
56
+ }
57
+
58
+ const rules = config.allow[pkgName] || [];
59
+ let allowed = rules.includes('.');
60
+ if (!allowed) {
61
+ for (const rule of rules) {
62
+ if (subPath === rule || subPath.startsWith(`${rule}/`)) {
63
+ allowed = true;
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ if (!allowed) {
69
+ return null;
70
+ }
71
+
72
+ let fsPath = path.resolve(config.root, rel);
73
+ if (/[\\/]$/.test(fsPath) && fsPath.length > 1) {
74
+ fsPath = fsPath.replace(/[\\/]+$/, '');
75
+ }
76
+ if (!fsPath.startsWith(config.root)) {
77
+ throw new Error('Resolved path is outside the root');
78
+ }
79
+
80
+ return fsPath;
81
+ };
82
+ }
83
+ }