woodland 22.0.3 → 22.0.5

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/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.
7
7
 
8
8
  [![npm version](https://badge.fury.io/js/woodland.svg)](https://badge.fury.io/js/woodland)
9
- [![Node.js Version](https://img.shields.io/node/v/woodland.svg)](https://nodejs.org/)
9
+ [![Node.js Version](https://img.shields.io/badge/node.js-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
10
10
  [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
11
- [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/avoidwork/woodland)
11
+ [![Test Coverage](https://img.shields.io/badge/coverage-100%25%20line-brightgreen.svg)](https://github.com/avoidwork/woodland)
12
12
 
13
13
  </div>
14
14
 
@@ -54,9 +54,10 @@ createServer(app.route).listen(3000, () => console.log("Server running at http:/
54
54
  - **Built-in security** - CORS, path traversal, XSS prevention (no additional packages)
55
55
  - **Secure by default** - CORS deny-all, path traversal blocked, HTML escaping automatic
56
56
  - **TypeScript first** - Full type definitions included
57
- - **No performance tradeoff** - Security features add ~0.02ms overhead per request
57
+ - **No performance tradeoff** - Security features add minimal overhead
58
58
  - **Lightweight** - Minimal dependencies (6 packages)
59
59
  - **Dual module support** - CommonJS and ESM
60
+ - **Production ready** - Event emitters for custom monitoring, examples for graceful shutdown
60
61
 
61
62
  **Built-in Security Features:**
62
63
 
@@ -69,21 +70,53 @@ createServer(app.route).listen(3000, () => console.log("Server running at http:/
69
70
 
70
71
  ## Common Patterns
71
72
 
72
- ### REST API
73
+ ### REST API with Error Handling
73
74
 
74
75
  ```javascript
75
76
  const users = new Map();
76
77
 
77
- app.get("/users", (req, res) => res.json(Array.from(users.values())));
78
+ app.get("/users", (req, res) => {
79
+ try {
80
+ const list = Array.from(users.values());
81
+ res.json(list);
82
+ } catch (error) {
83
+ res.error(500, error.message);
84
+ }
85
+ });
86
+
78
87
  app.get("/users/:id", (req, res) => {
79
- const user = users.get(req.params.id);
80
- user ? res.json(user) : res.error(404);
88
+ try {
89
+ const user = users.get(req.params.id);
90
+ if (!user) {
91
+ return res.error(404, "User not found");
92
+ }
93
+ res.json(user);
94
+ } catch (error) {
95
+ res.error(500, error.message);
96
+ }
81
97
  });
82
- app.post("/users", (req, res) => {
83
- const id = Date.now().toString();
84
- const user = { ...req.body, id };
85
- users.set(id, user);
86
- res.json(user, 201);
98
+
99
+ app.post("/users", async (req, res) => {
100
+ try {
101
+ const id = Date.now().toString();
102
+ const user = { ...req.body, id };
103
+ users.set(id, user);
104
+ res.status(201).json(user);
105
+ } catch (error) {
106
+ res.error(400, error.message);
107
+ }
108
+ });
109
+ ```
110
+
111
+ ### Health Check Endpoint
112
+
113
+ ```javascript
114
+ app.get("/health", (req, res) => {
115
+ res.json({
116
+ status: "healthy",
117
+ timestamp: new Date().toISOString(),
118
+ uptime: process.uptime(),
119
+ });
87
120
  });
88
121
  ```
89
122
 
@@ -108,17 +141,17 @@ app.files("/static", "./public");
108
141
  ```javascript
109
142
  // Global middleware
110
143
  app.always((req, res, next) => {
111
- req.startTime = Date.now();
112
- next();
144
+ req.startTime = Date.now();
145
+ next();
113
146
  });
114
147
 
115
148
  // Route-specific middleware
116
149
  app.get("/protected", authenticate, handler);
117
150
 
118
- // Error handler (register last, 4 params)
119
- app.use("/(.*)", (error, req, res, next) => {
120
- console.error(error);
121
- res.error(500);
151
+ // Error handler (register last with 4 params)
152
+ app.use((error, req, res, next) => {
153
+ console.error(error);
154
+ res.error(500, error.message);
122
155
  });
123
156
  ```
124
157
 
@@ -126,18 +159,18 @@ app.use("/(.*)", (error, req, res, next) => {
126
159
 
127
160
  ```javascript
128
161
  const app = woodland({
129
- origins: [], // CORS allowlist (empty = deny all)
130
- autoIndex: false, // Directory browsing
131
- etags: true, // ETag support
132
- cacheSize: 1000, // Route cache size
133
- cacheTTL: 10000, // Cache TTL in ms
134
- charset: "utf-8", // Default charset
135
- logging: {
136
- enabled: true,
137
- level: "info",
138
- },
139
- time: false, // X-Response-Time header
140
- silent: false, // Disable server headers
162
+ origins: process.env.ALLOWED_ORIGINS?.split(",") || [],
163
+ autoIndex: process.env.AUTO_INDEX === "true",
164
+ etags: true,
165
+ cacheSize: 1000,
166
+ cacheTTL: 10000,
167
+ charset: "utf-8",
168
+ logging: {
169
+ enabled: true,
170
+ level: process.env.LOG_LEVEL || "info",
171
+ },
172
+ time: true,
173
+ silent: process.env.NODE_ENV === "production",
141
174
  });
142
175
  ```
143
176
 
@@ -174,6 +207,36 @@ app.on("error", (req, res, err) => console.error(`Error ${res.statusCode}:`, err
174
207
  app.on("stream", (req, res) => console.log(`Streaming file`));
175
208
  ```
176
209
 
210
+ ## Graceful Shutdown
211
+
212
+ ```javascript
213
+ import { createServer } from "node:http";
214
+ import { woodland } from "woodland";
215
+
216
+ const app = woodland();
217
+ const server = createServer(app.route);
218
+
219
+ const gracefulShutdown = (signal) => {
220
+ console.log(`Received ${signal}, shutting down gracefully...`);
221
+
222
+ server.close(() => {
223
+ console.log("Server closed");
224
+ process.exit(0);
225
+ });
226
+
227
+ // Force shutdown after 30s
228
+ setTimeout(() => {
229
+ console.error("Forced shutdown");
230
+ process.exit(1);
231
+ }, 30000);
232
+ };
233
+
234
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
235
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
236
+
237
+ server.listen(3000);
238
+ ```
239
+
177
240
  ## CLI
178
241
 
179
242
  ```bash
@@ -190,7 +253,7 @@ npx woodland --ip=0.0.0.0
190
253
  ## Testing
191
254
 
192
255
  ```bash
193
- npm test # Run tests (100% line coverage)
256
+ npm test # Run tests (334 tests, 100% line, 99.37% function, 95.90% branch coverage)
194
257
  npm run coverage # Generate coverage report
195
258
  npm run benchmark # Performance benchmarks
196
259
  npm run lint # Check linting
@@ -198,14 +261,14 @@ npm run lint # Check linting
198
261
 
199
262
  ## Documentation
200
263
 
201
- - [API Reference](https://github.com/avoidwork/woodland/tree/master/docs/API.md) - Complete method documentation
202
- - [Technical Documentation](https://github.com/avoidwork/woodland/tree/master/docs/TECHNICAL_DOCUMENTATION.md) - Architecture, OWASP security, internals
203
- - [Code Style Guide](https://github.com/avoidwork/woodland/tree/master/docs/CODE_STYLE_GUIDE.md) - Conventions and best practices
204
- - [Benchmarks](https://github.com/avoidwork/woodland/tree/master/docs/BENCHMARKS.md) - Performance testing results
264
+ - [API Reference](docs/API.md) - Complete method documentation
265
+ - [Technical Documentation](docs/TECHNICAL_DOCUMENTATION.md) - Architecture, OWASP security, internals
266
+ - [Code Style Guide](docs/CODE_STYLE_GUIDE.md) - Conventions and best practices
267
+ - [Benchmarks](docs/BENCHMARKS.md) - Performance testing results
205
268
 
206
269
  ## Performance
207
270
 
208
- Woodland delivers **enterprise-grade security without sacrificing performance**. Security features add minimal overhead (~0.02ms per request).
271
+ Woodland delivers **enterprise-grade security without sacrificing performance**. Security features add minimal overhead.
209
272
 
210
273
  | Framework | Security Approach | Mean Response Time |
211
274
  |-----------|------------------|-------------------|
@@ -222,6 +285,8 @@ Woodland implements multiple layers of protection:
222
285
  3. **Input Validation** - IP addresses validated, URLs parsed securely
223
286
  4. **Output Encoding** - HTML escaping automatic
224
287
  5. **Secure Error Handling** - No internal paths exposed
288
+ 6. **Header Injection Prevention** - Type validation for headers
289
+ 7. **Prototype Pollution Protection** - Safe ETag generation
225
290
 
226
291
  **Production Setup:**
227
292
 
@@ -230,16 +295,37 @@ import helmet from "helmet";
230
295
  import rateLimit from "express-rate-limit";
231
296
 
232
297
  app.always(helmet());
233
- app.always(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
298
+ app.always(
299
+ rateLimit({
300
+ windowMs: 15 * 60 * 1000, // 15 minutes
301
+ max: 100, // Limit each IP to 100 requests
302
+ standardHeaders: true,
303
+ legacyHeaders: false,
304
+ }),
305
+ );
234
306
  ```
235
307
 
308
+ **Security Warning:**
309
+ > ⚠️ **Production Deployment**: Always use a reverse proxy (nginx, Cloudflare) in production for SSL/TLS termination, DDoS protection, and additional security layers.
310
+
236
311
  ## License
237
312
 
238
313
  Copyright (c) 2026 Jason Mulligan
239
314
 
240
315
  Licensed under the **BSD-3-Clause** license.
241
316
 
317
+ ## Contributing
318
+
319
+ Contributions are welcome!
320
+
321
+ 1. Fork the repository
322
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
323
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
324
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
325
+ 5. Open a Pull Request
326
+
242
327
  ## Support
243
328
 
244
329
  - **Issues**: [GitHub Issues](https://github.com/avoidwork/woodland/issues)
245
330
  - **Discussions**: [GitHub Discussions](https://github.com/avoidwork/woodland/discussions)
331
+ - **Email**: jason@mulligan.me
package/dist/cli.cjs CHANGED
@@ -4,17 +4,17 @@
4
4
  *
5
5
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
6
6
  * @license BSD-3-Clause
7
- * @version 22.0.3
7
+ * @version 22.0.5
8
8
  */
9
9
  'use strict';
10
10
 
11
11
  var node_http = require('node:http');
12
- var node_url = require('node:url');
13
- var node_path = require('node:path');
14
- var tinyCoerce = require('tiny-coerce');
15
12
  var woodland = require('woodland');
16
13
  var node_module = require('node:module');
14
+ var node_path = require('node:path');
15
+ var node_url = require('node:url');
17
16
  var mimeDb = require('mime-db');
17
+ var tinyCoerce = require('tiny-coerce');
18
18
 
19
19
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
20
20
  const __dirname$1 = node_url.fileURLToPath(new node_url.URL(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
@@ -37,20 +37,23 @@ const INT_10 = 10;
37
37
  const INT_255 = 255;
38
38
  const INT_8000 = 8000;
39
39
  const INT_65535 = 65535;
40
+ const INT_NEG_1 = -1;
40
41
  const COLON = ":";
41
42
  const DOUBLE_COLON = "::";
42
43
  const EMPTY = "";
43
44
  const EQUAL = "=";
44
45
  const HYPHEN = "-";
45
- const WOODLAND = "woodland";
46
46
  const STRING = "string";
47
47
  `nodejs/${process.version}, ${process.platform}/${process.arch}`;
48
48
  const LOCALHOST = "127.0.0.1";
49
49
  const EXTENSIONS = "extensions";
50
50
  const INFO = "info";
51
+ const MSG_INVALID_IP = "Invalid IP: must be a valid IPv4 or IPv6 address.";
52
+ const MSG_INVALID_PORT = "Invalid port: must be an integer between 0 and 65535.";
51
53
  const NO_CACHE = "no-cache";
52
54
  const EN_US = "en-US";
53
55
  const SHORT = "short";
56
+ const EVT_LISTENING = "listening";
54
57
 
55
58
  Object.freeze(
56
59
  Array.from({ length: 12 }, (_, idx) => {
@@ -60,56 +63,55 @@ Object.freeze(
60
63
  return Object.freeze(d.toLocaleString(EN_US, { month: SHORT }));
61
64
  }),
62
65
  );
66
+ const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
67
+ const IPV6_CHAR_PATTERN = /^[0-9a-fA-F:.]+$/;
68
+ const IPV4_MAPPED_PATTERN = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;
69
+ const HEX_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
63
70
 
64
- const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[1]);
71
+ const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[INT_1]);
65
72
  valid.reduce((a, v) => {
66
- const result = Object.assign({ type: v[0] }, v[1]);
73
+ const result = Object.assign({ type: v[INT_0] }, v[INT_1]);
67
74
  const extCount = result.extensions.length;
68
- for (let i = 0; i < extCount; i++) {
75
+ for (let i = INT_0; i < extCount; i++) {
69
76
  a[`.${result.extensions[i]}`] = result;
70
77
  }
71
78
  return a;
72
79
  }, {});
73
80
 
74
- const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
75
- IPV6_CHAR_PATTERN = /^[0-9a-fA-F:.]+$/,
76
- IPV4_MAPPED_PATTERN = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i,
77
- HEX_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
78
-
79
81
  /**
80
- * Validates if an IP address is properly formatted
81
- * @param {string} ip - IP address to validate
82
- * @returns {boolean} True if IP is valid format
82
+ * Validates IPv4 address format
83
+ * @param {string} ip - IPv4 address to validate
84
+ * @returns {boolean} True if valid IPv4
83
85
  */
84
- function isValidIP(ip) {
85
- if (!ip || typeof ip !== STRING) {
86
+ function isValidIPv4(ip) {
87
+ const match = IPV4_PATTERN.exec(ip);
88
+ if (!match) {
86
89
  return false;
87
90
  }
88
91
 
89
- if (ip.indexOf(COLON) === -1) {
90
- const match = IPV4_PATTERN.exec(ip);
91
-
92
- if (!match) {
92
+ for (let i = INT_1; i < INT_5; i++) {
93
+ const num = parseInt(match[i], INT_10);
94
+ if (num > INT_255) {
93
95
  return false;
94
96
  }
95
-
96
- for (let i = 1; i < INT_5; i++) {
97
- const num = parseInt(match[i], INT_10);
98
- if (num > INT_255) {
99
- return false;
100
- }
101
- }
102
-
103
- return true;
104
97
  }
105
98
 
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Validates IPv6 address format
104
+ * @param {string} ip - IPv6 address to validate
105
+ * @returns {boolean} True if valid IPv6
106
+ */
107
+ function isValidIPv6(ip) {
106
108
  if (!IPV6_CHAR_PATTERN.test(ip)) {
107
109
  return false;
108
110
  }
109
111
 
110
112
  const ipv4MappedMatch = IPV4_MAPPED_PATTERN.exec(ip);
111
113
  if (ipv4MappedMatch) {
112
- return isValidIP(ipv4MappedMatch[1]);
114
+ return isValidIPv4(ipv4MappedMatch[INT_1]);
113
115
  }
114
116
 
115
117
  if (ip === DOUBLE_COLON) {
@@ -117,75 +119,95 @@ function isValidIP(ip) {
117
119
  }
118
120
 
119
121
  const doubleColonIndex = ip.indexOf(DOUBLE_COLON);
120
- const isCompressed = doubleColonIndex !== -1;
122
+ const isCompressed = doubleColonIndex !== INT_NEG_1;
121
123
 
122
124
  if (isCompressed) {
123
- if (ip.indexOf(DOUBLE_COLON, doubleColonIndex + INT_2) !== -1) {
124
- return false;
125
- }
125
+ return validateCompressedIPv6(ip, doubleColonIndex);
126
+ }
126
127
 
127
- if (
128
- (doubleColonIndex > INT_0 && ip.charAt(doubleColonIndex - INT_1) === COLON) ||
129
- (doubleColonIndex + INT_2 < ip.length && ip.charAt(doubleColonIndex + INT_2) === COLON)
130
- ) {
131
- return false;
132
- }
128
+ return validateUncompressedIPv6(ip);
129
+ }
133
130
 
134
- const beforeDoubleColon = ip.substring(INT_0, doubleColonIndex);
135
- const afterDoubleColon = ip.substring(doubleColonIndex + INT_2);
131
+ /**
132
+ * Validates compressed IPv6 address (with ::)
133
+ * @param {string} ip - IPv6 address
134
+ * @param {number} doubleColonIndex - Position of ::
135
+ * @returns {boolean} True if valid
136
+ */
137
+ function validateCompressedIPv6(ip, doubleColonIndex) {
138
+ if (ip.indexOf(DOUBLE_COLON, doubleColonIndex + INT_2) !== INT_NEG_1) {
139
+ return false;
140
+ }
136
141
 
137
- let leftGroups;
138
- if (beforeDoubleColon) {
139
- leftGroups = beforeDoubleColon.split(COLON);
140
- } else {
141
- leftGroups = [];
142
- }
142
+ if (
143
+ (doubleColonIndex > INT_0 && ip.charAt(doubleColonIndex - INT_1) === COLON) ||
144
+ (doubleColonIndex + INT_2 < ip.length && ip.charAt(doubleColonIndex + INT_2) === COLON)
145
+ ) {
146
+ return false;
147
+ }
143
148
 
144
- let rightGroups;
145
- if (afterDoubleColon) {
146
- rightGroups = afterDoubleColon.split(COLON);
147
- } else {
148
- rightGroups = [];
149
- }
149
+ const beforeDoubleColon = ip.substring(INT_0, doubleColonIndex);
150
+ const afterDoubleColon = ip.substring(doubleColonIndex + INT_2);
150
151
 
151
- const nonEmptyLeft = leftGroups.filter((g) => g !== EMPTY);
152
- const nonEmptyRight = rightGroups.filter((g) => g !== EMPTY);
153
- const totalGroups = nonEmptyLeft.length + nonEmptyRight.length;
152
+ const leftGroups = beforeDoubleColon ? beforeDoubleColon.split(COLON) : [];
153
+ const rightGroups = afterDoubleColon ? afterDoubleColon.split(COLON) : [];
154
154
 
155
- if (totalGroups >= INT_8) {
156
- return false;
157
- }
155
+ const totalGroups =
156
+ leftGroups.filter((g) => g !== EMPTY).length + rightGroups.filter((g) => g !== EMPTY).length;
158
157
 
159
- /* node:coverage ignore next 5 */
160
- for (let i = INT_0; i < nonEmptyLeft.length; i++) {
161
- if (!HEX_GROUP_PATTERN.test(nonEmptyLeft[i])) {
162
- return false;
163
- }
164
- }
158
+ if (totalGroups >= INT_8) {
159
+ return false;
160
+ }
165
161
 
166
- /* node:coverage ignore next 5 */
167
- for (let i = INT_0; i < nonEmptyRight.length; i++) {
168
- if (!HEX_GROUP_PATTERN.test(nonEmptyRight[i])) {
169
- return false;
170
- }
171
- }
162
+ return validateHexGroups(leftGroups) && validateHexGroups(rightGroups);
163
+ }
172
164
 
173
- return true;
174
- } else {
175
- const groups = ip.split(COLON);
176
- if (groups.length !== INT_8) {
165
+ /**
166
+ * Validates uncompressed IPv6 address
167
+ * @param {string} ip - IPv6 address
168
+ * @returns {boolean} True if valid
169
+ */
170
+ function validateUncompressedIPv6(ip) {
171
+ const groups = ip.split(COLON);
172
+ if (groups.length !== INT_8) {
173
+ return false;
174
+ }
175
+
176
+ return validateHexGroups(groups);
177
+ }
178
+
179
+ /**
180
+ * Validates hex groups in IPv6 address
181
+ * @param {Array} groups - Array of hex group strings
182
+ * @returns {boolean} True if all groups are valid
183
+ */
184
+ function validateHexGroups(groups) {
185
+ const groupCount = groups.length;
186
+
187
+ for (let i = INT_0; i < groupCount; i++) {
188
+ /* node:coverage ignore next 3 */
189
+ if (!groups[i] || !HEX_GROUP_PATTERN.test(groups[i])) {
177
190
  return false;
178
191
  }
192
+ }
193
+ return true;
194
+ }
179
195
 
180
- /* node:coverage ignore next 5 */
181
- for (let i = INT_0; i < INT_8; i++) {
182
- if (!groups[i] || !HEX_GROUP_PATTERN.test(groups[i])) {
183
- return false;
184
- }
185
- }
196
+ /**
197
+ * Validates if an IP address is properly formatted
198
+ * @param {string} ip - IP address to validate
199
+ * @returns {boolean} True if IP is valid format
200
+ */
201
+ function isValidIP(ip) {
202
+ if (!ip || typeof ip !== STRING) {
203
+ return false;
204
+ }
186
205
 
187
- return true;
206
+ if (ip.indexOf(COLON) === INT_NEG_1) {
207
+ return isValidIPv4(ip);
188
208
  }
209
+
210
+ return isValidIPv6(ip);
189
211
  }
190
212
 
191
213
  /**
@@ -195,10 +217,10 @@ function isValidIP(ip) {
195
217
  */
196
218
  function parseArgs(args) {
197
219
  return args
198
- .filter((i) => i.charAt(0) === HYPHEN && i.charAt(1) === HYPHEN)
220
+ .filter((i) => i.charAt(INT_0) === HYPHEN && i.charAt(INT_1) === HYPHEN)
199
221
  .reduce((a, v) => {
200
- const x = v.split(`${HYPHEN}${HYPHEN}`)[1].split(EQUAL);
201
- a[x[0]] = tinyCoerce.coerce(x[1]);
222
+ const x = v.split(`${HYPHEN}${HYPHEN}`)[INT_1].split(EQUAL);
223
+ a[x[INT_0]] = tinyCoerce.coerce(x[INT_1]);
202
224
  return a;
203
225
  }, {});
204
226
  }
@@ -209,13 +231,12 @@ function parseArgs(args) {
209
231
  * @returns {Object} Validation result with valid flag and error message
210
232
  */
211
233
  function validatePort(port) {
212
- // Reject empty strings and whitespace-only values
213
234
  if (port === EMPTY || (typeof port === STRING && port.trim() === EMPTY)) {
214
- return { valid: false, error: "Invalid port: must be an integer between 0 and 65535." };
235
+ return { valid: false, error: MSG_INVALID_PORT };
215
236
  }
216
237
  const validPort = Number(port);
217
238
  if (!Number.isInteger(validPort) || validPort < INT_0 || validPort > INT_65535) {
218
- return { valid: false, error: "Invalid port: must be an integer between 0 and 65535." };
239
+ return { valid: false, error: MSG_INVALID_PORT };
219
240
  }
220
241
  return { valid: true, port: validPort };
221
242
  }
@@ -228,7 +249,7 @@ function validatePort(port) {
228
249
  function validateIP(ip) {
229
250
  const validIP = isValidIP(ip);
230
251
  if (!validIP) {
231
- return { valid: false, error: "Invalid IP: must be a valid IPv4 or IPv6 address." };
252
+ return { valid: false, error: MSG_INVALID_IP };
232
253
  }
233
254
  return { valid: true, ip };
234
255
  }
@@ -270,7 +291,7 @@ function main(args = process.argv) {
270
291
  const server = node_http.createServer(app.route);
271
292
  server.listen(portValidation.port, ip);
272
293
  /* node:coverage ignore next 6 */
273
- server.on("listening", () => {
294
+ server.on(EVT_LISTENING, () => {
274
295
  const actualPort = server.address().port;
275
296
  app.logger.log(
276
297
  `id=woodland, hostname=${process.env.HOSTNAME ?? "localhost"}, ip=${ip}, port=${actualPort}`,
@@ -281,17 +302,8 @@ function main(args = process.argv) {
281
302
  return server;
282
303
  }
283
304
 
284
- // CLI entry point - only run when executed directly
285
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
286
- /* node:coverage ignore next 6 */
287
- if (process.argv[1]) {
288
- const scriptPath = node_path.resolve(process.argv[1]);
289
- if (scriptPath === __filename$1 || node_path.basename(scriptPath) === WOODLAND) {
290
- main();
291
- }
292
- }
305
+ // CLI entry point - always run main
306
+ /* node:coverage ignore next */
307
+ main();
293
308
 
294
309
  exports.main = main;
295
- exports.parseArgs = parseArgs;
296
- exports.validateIP = validateIP;
297
- exports.validatePort = validatePort;