@catbee/utils 2.0.0 → 2.0.2

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/server/index.mjs CHANGED
@@ -31,9 +31,9 @@ import { Env } from '@catbee/utils/env';
31
31
  import { getLogger } from '@catbee/utils/logger';
32
32
  import { ServiceUnavailableException, InternalServerErrorException, NotFoundException } from '@catbee/utils/exception';
33
33
  import { getCatbeeServerGlobalConfig } from '@catbee/utils/config';
34
- import { deepObjMerge, deepClone } from '@catbee/utils/object';
34
+ import { deepObjMerge, isPlainObject, deepClone } from '@catbee/utils/object';
35
35
  import { fileExists, readFile, readFileSync } from '@catbee/utils/fs';
36
- import { isPort } from '@catbee/utils/validation';
36
+ import { isPort, isHostname } from '@catbee/utils/validation';
37
37
  import { optionalRequire } from '@catbee/utils/async';
38
38
  import { uuid } from '@catbee/utils/id';
39
39
 
@@ -50,17 +50,29 @@ var ServerConfigBuilder = class {
50
50
  *
51
51
  * @private
52
52
  * @param port - The port number to validate
53
- * @throws {Error} If port is not an integer or is outside the valid range (1-65535)
53
+ * @throws {Error} If port is not an integer or is outside the valid range (0-65535)
54
54
  */
55
55
  validatePort(port) {
56
- if (!isPort(port)) {
57
- throw new Error(`Port must be a valid number between 1 and 65535, got: ${port}`);
56
+ if (!isPort(port, true)) {
57
+ throw new Error(`Port must be a valid number between 0 and 65535, got: ${port}`);
58
+ }
59
+ }
60
+ /**
61
+ * Validates that a hostname is valid.
62
+ *
63
+ * @private
64
+ * @param host - The hostname to validate
65
+ * @throws {Error} If hostname is invalid
66
+ */
67
+ validateHost(host) {
68
+ if (!isHostname(host)) {
69
+ throw new Error(`Host must be a valid hostname or IP address, got: ${host}`);
58
70
  }
59
71
  }
60
72
  /**
61
73
  * Sets the port the server will listen on.
62
74
  *
63
- * @param port - The port number (1-65535)
75
+ * @param port - The port number (0-65535). Use 0 for dynamic port assignment.
64
76
  * @returns The builder instance for chaining
65
77
  * @throws {Error} If port is invalid
66
78
  * @default 3000 (can be overridden via PORT env variable)
@@ -80,14 +92,17 @@ var ServerConfigBuilder = class {
80
92
  *
81
93
  * @param host - The hostname (e.g., 'localhost', '0.0.0.0', '127.0.0.1')
82
94
  * @returns The builder instance for chaining
95
+ * @throws {Error} If hostname is invalid
83
96
  * @default '0.0.0.0' (can be overridden via HOST env variable)
84
97
  *
85
98
  * @example
86
99
  * ```typescript
87
100
  * builder.withHost('0.0.0.0') // Listen on all interfaces
101
+ * builder.withHost('localhost') // Listen on localhost only
88
102
  * ```
89
103
  */
90
104
  withHost(host) {
105
+ this.validateHost(host);
91
106
  this.config.host = host;
92
107
  return this;
93
108
  }
@@ -498,12 +513,23 @@ var ServerConfigBuilder = class {
498
513
  * ```
499
514
  */
500
515
  withBodyParser(opts) {
516
+ if (opts === true) {
517
+ this.config.bodyParser = getCatbeeServerGlobalConfig().bodyParser;
518
+ return this;
519
+ } else if (opts === false) {
520
+ this.config.bodyParser = false;
521
+ return this;
522
+ }
501
523
  this.config.bodyParser = {
502
524
  ...this.config.bodyParser,
503
525
  ...opts
504
526
  };
505
527
  return this;
506
528
  }
529
+ disableBodyParser() {
530
+ this.config.bodyParser = false;
531
+ return this;
532
+ }
507
533
  /**
508
534
  * Configures cookie parsing middleware.
509
535
  *
@@ -654,7 +680,7 @@ var ServerConfigBuilder = class {
654
680
  });
655
681
  }
656
682
  mergeConfig(key, value) {
657
- const current = this.config[key] && typeof this.config[key] === "object" ? deepClone(this.config[key]) : {};
683
+ const current = isPlainObject(this.config[key]) ? deepClone(this.config[key]) : {};
658
684
  this.config[key] = deepObjMerge({}, current, value);
659
685
  }
660
686
  setEnabled(key, enable, overrides = {}) {
@@ -735,12 +761,15 @@ var ExpressServer = class {
735
761
  */
736
762
  constructor(config, hooks = {}) {
737
763
  if (this.hasBuildMarker(config)) {
738
- this.config = config;
764
+ this.config = deepObjMerge({}, config);
739
765
  } else {
740
766
  this.config = deepObjMerge({}, getCatbeeServerGlobalConfig(), config);
741
767
  }
742
- if (!isPort(this.config.port)) {
743
- const msg = `Port must be a valid number between 1 and 65535, got: ${this.config.port}`;
768
+ if (this.config.host) {
769
+ this.config.host = this.normalizeHost(this.config.host);
770
+ }
771
+ if (!isPort(this.config.port, true)) {
772
+ const msg = `Port must be a valid number between 0 and 65535, got: ${this.config.port}`;
744
773
  getLogger().error(msg);
745
774
  throw new Error(msg);
746
775
  }
@@ -853,6 +882,7 @@ var ExpressServer = class {
853
882
  async initialize() {
854
883
  await this.runHook("beforeInit", this);
855
884
  await this.setupMiddleware();
885
+ await this.runHook("beforeRoutes", this.app);
856
886
  await this.setupRoutes();
857
887
  await this.runHook("afterInit", this);
858
888
  }
@@ -1018,7 +1048,6 @@ var ExpressServer = class {
1018
1048
  }
1019
1049
  const logger = getLogger();
1020
1050
  const incomingRequestMetaData = {
1021
- requestId: req.id,
1022
1051
  method: req.method,
1023
1052
  url: req.originalUrl || req.url,
1024
1053
  ip: req.ip
@@ -1068,13 +1097,23 @@ var ExpressServer = class {
1068
1097
  * Set up body parsing middleware.
1069
1098
  */
1070
1099
  setupBodyParsingMiddleware() {
1071
- if (this.config.bodyParser) {
1100
+ if (isPlainObject(this.config.bodyParser)) {
1072
1101
  if (this.config.bodyParser.json) {
1073
1102
  this.app.use(express.json(this.config.bodyParser.json));
1074
1103
  }
1075
1104
  if (this.config.bodyParser.urlencoded) {
1076
1105
  this.app.use(express.urlencoded(this.config.bodyParser.urlencoded));
1077
1106
  }
1107
+ } else if (this.config.bodyParser === true) {
1108
+ const globalBodyParserConfig = getCatbeeServerGlobalConfig().bodyParser;
1109
+ if (isPlainObject(globalBodyParserConfig)) {
1110
+ if (globalBodyParserConfig.json) {
1111
+ this.app.use(express.json(globalBodyParserConfig.json));
1112
+ }
1113
+ if (globalBodyParserConfig.urlencoded) {
1114
+ this.app.use(express.urlencoded(globalBodyParserConfig.urlencoded));
1115
+ }
1116
+ }
1078
1117
  }
1079
1118
  }
1080
1119
  /**
@@ -1201,6 +1240,7 @@ var ExpressServer = class {
1201
1240
  }
1202
1241
  const routerToUse = this.externalRouter || this.rootRouter;
1203
1242
  this.app.use(this.globalPrefix, routerToUse);
1243
+ await this.runHook("afterRoutes", this.app);
1204
1244
  this.app.use((req, res) => {
1205
1245
  const status = HttpStatusCodes.NOT_FOUND;
1206
1246
  const response = createFinalErrorResponse(req, status, `Route ${req.method.toUpperCase()} ${req.path} not found`);
@@ -1354,6 +1394,7 @@ var ExpressServer = class {
1354
1394
  resolve(this.server);
1355
1395
  }, "onListening");
1356
1396
  this.server = this.createServerInstance(onListening);
1397
+ this.runHook("onServerCreated", this.server);
1357
1398
  this.setupConnectionTracking();
1358
1399
  this.setupServerErrorHandling(reject);
1359
1400
  } catch (error) {
@@ -1411,7 +1452,9 @@ var ExpressServer = class {
1411
1452
  */
1412
1453
  logServerStartInfo() {
1413
1454
  const protocol = this.config.https ? "https" : "http";
1414
- const url = `${protocol}://${this.config.host}:${this.config.port}`;
1455
+ const port = this.getPort();
1456
+ const host = this.formatHostForUrl(this.config.host || "localhost");
1457
+ const url = `${protocol}://${host}:${port}`;
1415
1458
  getLogger().info(`Server running on ${url}`);
1416
1459
  if (this.config.healthCheck?.path) {
1417
1460
  getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
@@ -1636,12 +1679,136 @@ var ExpressServer = class {
1636
1679
  return this.config;
1637
1680
  }
1638
1681
  /**
1682
+ * Get the port the server is listening on.
1683
+ * Returns the actual port if server is running (useful when config.port was 0),
1684
+ * otherwise returns the configured port.
1685
+ *
1686
+ * @returns The port number
1687
+ */
1688
+ getPort() {
1689
+ const address = this.server?.address();
1690
+ if (address && typeof address === "object" && "port" in address) {
1691
+ return address.port;
1692
+ }
1693
+ return this.config.port;
1694
+ }
1695
+ /**
1696
+ * Get the full URL the server is running on.
1697
+ * Returns the actual URL if server is running (useful when config.port was 0),
1698
+ * otherwise returns the configured URL.
1699
+ *
1700
+ * @returns The full server URL (e.g., "http://localhost:3000")
1701
+ */
1702
+ getUrl() {
1703
+ const protocol = this.config.https ? "https" : "http";
1704
+ const port = this.getPort();
1705
+ const host = this.formatHostForUrl(this.config.host || "localhost");
1706
+ return `${protocol}://${host}:${port}`;
1707
+ }
1708
+ /**
1709
+ * Check if the server is configured for dynamic port assignment.
1710
+ * Returns true if the original port configuration was 0.
1711
+ *
1712
+ * @returns True if using dynamic port assignment, false otherwise
1713
+ */
1714
+ isPortDynamic() {
1715
+ return this.config.port === 0;
1716
+ }
1717
+ /**
1718
+ * Check if the server is currently running and listening for requests.
1719
+ *
1720
+ * @returns True if server is running, false otherwise
1721
+ */
1722
+ isRunning() {
1723
+ return this.server !== null && this.server.listening;
1724
+ }
1725
+ /**
1726
+ * Get the host address the server is bound to.
1727
+ *
1728
+ * @returns The host address
1729
+ */
1730
+ getHost() {
1731
+ return this.config.host || "localhost";
1732
+ }
1733
+ /**
1734
+ * Get the protocol the server is using ('http' or 'https').
1735
+ *
1736
+ * @returns The protocol string
1737
+ */
1738
+ getProtocol() {
1739
+ return this.config.https ? "https" : "http";
1740
+ }
1741
+ /**
1742
+ * Check if the server is configured to use HTTPS.
1743
+ *
1744
+ * @returns True if using HTTPS, false otherwise
1745
+ */
1746
+ isHttps() {
1747
+ return this.config.https !== void 0;
1748
+ }
1749
+ /**
1750
+ * Set a new port for the server.
1751
+ * Can only be called before the server starts listening.
1752
+ * Useful for testing scenarios where you need to change the port dynamically.
1753
+ *
1754
+ * @param port - The new port number (0-65535)
1755
+ * @throws Error if server is already running or port is invalid
1756
+ */
1757
+ setPort(port) {
1758
+ if (this.server) {
1759
+ throw new Error("Cannot change port after server has started");
1760
+ }
1761
+ if (!isPort(port, true)) {
1762
+ throw new Error(`Port must be a valid number between 0 and 65535, got: ${port}`);
1763
+ }
1764
+ this.config.port = port;
1765
+ }
1766
+ /**
1767
+ * Set a new host for the server.
1768
+ * Can only be called before the server starts listening.
1769
+ * Useful for testing scenarios where you need to change the host dynamically.
1770
+ *
1771
+ * @param host - The new host (e.g., "localhost", "0.0.0.0", "127.0.0.1", "::1", or "[::1]")
1772
+ * @throws {Error} If server is already running or host is invalid
1773
+ */
1774
+ setHost(host) {
1775
+ if (this.server) {
1776
+ throw new Error("Cannot change host after server has started");
1777
+ }
1778
+ const normalizedHost = this.normalizeHost(host);
1779
+ if (!isHostname(normalizedHost)) {
1780
+ throw new Error(`Host must be a valid hostname or IP address, got: ${host}`);
1781
+ }
1782
+ this.config.host = normalizedHost;
1783
+ }
1784
+ /**
1639
1785
  * Wait until server initialization (middleware + routes) has completed.
1640
1786
  * Useful for integration tests that inspect app before starting.
1641
1787
  */
1642
1788
  async waitUntilReady() {
1643
1789
  await this.initPromise;
1644
1790
  }
1791
+ /**
1792
+ * Normalize host by stripping surrounding brackets from IPv6 addresses.
1793
+ * This ensures the host value is compatible with server.listen().
1794
+ * Brackets are URL syntax only and must be removed for Node.js binding.
1795
+ */
1796
+ normalizeHost(host) {
1797
+ if (host.startsWith("[") && host.endsWith("]")) {
1798
+ return host.slice(1, -1);
1799
+ }
1800
+ return host;
1801
+ }
1802
+ /**
1803
+ * Format host for use in URLs.
1804
+ * Wraps IPv6 addresses in brackets per RFC 3986.
1805
+ */
1806
+ formatHostForUrl(host) {
1807
+ if (host.includes(":")) {
1808
+ return `[${host}]`;
1809
+ }
1810
+ return host;
1811
+ }
1645
1812
  normalizePath(path, withGlobalPrefix = false) {
1646
1813
  const sanitize = /* @__PURE__ */ __name((p) => {
1647
1814
  return "/" + p.trim().replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
package/types/index.d.ts CHANGED
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { json, urlencoded, Request, Response, Express, NextFunction } from 'express';
26
26
  import http from 'node:http';
27
+ import https from 'node:https';
27
28
  import { ExpressServer } from '@catbee/utils/server';
28
29
  import { HelmetOptions } from 'helmet';
29
30
  import { CompressionOptions } from 'compression';
@@ -165,6 +166,7 @@ interface CatbeeServerConfig {
165
166
  /** Server port
166
167
  * - **default**: `3000`
167
168
  * - **env**: `SERVER_PORT` || `PORT`
169
+ * - Use `0` for dynamic port assignment (OS assigns available port)
168
170
  */
169
171
  port: number;
170
172
  /** Host address to bind the server
@@ -225,7 +227,7 @@ interface CatbeeServerConfig {
225
227
  * - `SERVER_BODY_PARSER_JSON_LIMIT`
226
228
  * - `SERVER_BODY_PARSER_URLENCODED_LIMIT`
227
229
  */
228
- bodyParser?: {
230
+ bodyParser?: ToggleConfig<{
229
231
  /** JSON body parser options
230
232
  * - **default**: `{ limit: '1mb' }`
231
233
  */
@@ -234,7 +236,7 @@ interface CatbeeServerConfig {
234
236
  * - **default**: `{ extended: true, limit: '1mb' }`
235
237
  */
236
238
  urlencoded?: Parameters<typeof urlencoded>[0];
237
- };
239
+ }>;
238
240
  /** Cookie parser toggle or options
239
241
  * - **default**: `false`
240
242
  * - **env**: `SERVER_COOKIE_PARSER_ENABLE`
@@ -554,8 +556,14 @@ interface CatbeeServerConfig {
554
556
  interface CatbeeServerHooks {
555
557
  /** Called before middleware & routes initialize */
556
558
  beforeInit?: (server: ExpressServer) => Promise<void> | void;
559
+ /** Called before any routes are registered */
560
+ beforeRoutes?: (app: Express) => Promise<void> | void;
561
+ /** Called after all routes are registered, before error handling middleware */
562
+ afterRoutes?: (app: Express) => Promise<void> | void;
557
563
  /** Called after middleware & routes initialize */
558
564
  afterInit?: (server: ExpressServer) => Promise<void> | void;
565
+ /** Called when the underlying HTTP/HTTPS server instance is created */
566
+ onServerCreated?: (server: http.Server | https.Server) => Promise<void> | void;
559
567
  /** Called before server starts listening */
560
568
  beforeStart?: (app: Express) => Promise<void> | void;
561
569
  /** Called after server is ready */
@@ -42,13 +42,15 @@ var BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}
42
42
  var DOT_ATOM_REGEX = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*$/;
43
43
  var QUOTED_LOCAL_REGEX = /^"([\s\x21\x23-\x5B\x5D-\x7E]|\\[\x20-\x7E])*"$/;
44
44
  var TLD_REGEX = /^[A-Za-z]{2,63}$/;
45
- function isPort(value) {
46
- if (typeof value === "number") return Number.isInteger(value) && value > 0 && value <= 65535;
45
+ function isPort(value, allowZero = false) {
46
+ const minPort = allowZero ? 0 : 1;
47
+ const maxPort = 65535;
48
+ if (typeof value === "number") return Number.isInteger(value) && value >= minPort && value <= maxPort;
47
49
  if (!value || typeof value !== "string") return false;
48
50
  const str = value.trim();
49
51
  if (!PORT_REGEX.test(str)) return false;
50
52
  const port = Number(str);
51
- return Number.isInteger(port) && port > 0 && port <= 65535;
53
+ return Number.isInteger(port) && port >= minPort && port <= maxPort;
52
54
  }
53
55
  __name(isPort, "isPort");
54
56
  function isEmail(str) {
@@ -154,7 +156,7 @@ function isIPv4(str) {
154
156
  const parts = str.split(".");
155
157
  if (parts.length !== 4) return false;
156
158
  return parts.every((p) => {
157
- if (p.length > 3 || p.startsWith("0") && p.length > 1) return false;
159
+ if (!p || p.length > 3 || p.startsWith("0") && p.length > 1) return false;
158
160
  const n = Number(p);
159
161
  return Number.isInteger(n) && n >= 0 && n <= 255;
160
162
  });
@@ -169,6 +171,29 @@ function isIPv6(str) {
169
171
  }
170
172
  }
171
173
  __name(isIPv6, "isIPv6");
174
+ function isHostname(str) {
175
+ if (!str || typeof str !== "string") return false;
176
+ let input = str.trim();
177
+ if (!input || input.length > 255) return false;
178
+ if (input.endsWith(".")) {
179
+ input = input.slice(0, -1);
180
+ }
181
+ if (isIPv4(input)) return true;
182
+ if (isIPv6(input)) return true;
183
+ if (input === "localhost") return true;
184
+ if (/^\d+(\.\d+){3,}$/.test(input)) {
185
+ return false;
186
+ }
187
+ if (input.includes("..")) return false;
188
+ const labels = input.split(".");
189
+ if (labels.length === 0) return false;
190
+ const labelRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
191
+ return labels.every((label) => {
192
+ if (!label || label.length > 63) return false;
193
+ return labelRegex.test(label);
194
+ });
195
+ }
196
+ __name(isHostname, "isHostname");
172
197
  function isCreditCard(str) {
173
198
  if (!str || typeof str !== "string") return false;
174
199
  const s = str.replace(/\D/g, "");
@@ -243,6 +268,7 @@ exports.isCreditCard = isCreditCard;
243
268
  exports.isDateInRange = isDateInRange;
244
269
  exports.isEmail = isEmail;
245
270
  exports.isHexColor = isHexColor;
271
+ exports.isHostname = isHostname;
246
272
  exports.isIPv4 = isIPv4;
247
273
  exports.isIPv6 = isIPv6;
248
274
  exports.isISODate = isISODate;
@@ -28,10 +28,11 @@
28
28
  /**
29
29
  * Checks if a string is a valid port number.
30
30
  *
31
- * @param str - The input string or number.
31
+ * @param value - The input string or number.
32
+ * @param allowZero - Whether to allow zero as a valid port number.
32
33
  * @returns True if valid port number, else false.
33
34
  */
34
- declare function isPort(value: string | number): boolean;
35
+ declare function isPort(value: string | number, allowZero?: boolean): boolean;
35
36
  /**
36
37
  * Checks if a string is a valid email address.
37
38
  *
@@ -142,6 +143,31 @@ declare function isIPv4(str: string): boolean;
142
143
  * @returns {boolean} True if valid IPv6 address.
143
144
  */
144
145
  declare function isIPv6(str: string): boolean;
146
+ /**
147
+ * Checks if a string is a valid hostname or IP address.
148
+ *
149
+ * Validates:
150
+ * - IPv4 addresses (e.g., '192.168.1.1')
151
+ * - IPv6 addresses (e.g., '::1', '2001:db8::1')
152
+ * - Domain names (e.g., 'example.com', 'sub.example.com')
153
+ * - Localhost variants ('localhost', '0.0.0.0', '127.0.0.1')
154
+ *
155
+ * @param {string} str - The hostname or IP address to validate.
156
+ * @returns {boolean} True if valid hostname or IP address.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * isHostname('localhost'); // true
161
+ * isHostname('0.0.0.0'); // true
162
+ * isHostname('192.168.1.1'); // true
163
+ * isHostname('example.com'); // true
164
+ * isHostname('sub.example.com'); // true
165
+ * isHostname('::1'); // true
166
+ * isHostname('invalid..com'); // false
167
+ * isHostname(''); // false
168
+ * ```
169
+ */
170
+ declare function isHostname(str: string): boolean;
145
171
  /**
146
172
  * Validates a credit card number using the Luhn algorithm.
147
173
  *
@@ -206,4 +232,4 @@ declare function matchesPattern(str: string, pattern: RegExp): boolean;
206
232
  */
207
233
  declare function validateAll(value: unknown, validators: Array<(value: unknown) => boolean>): boolean;
208
234
 
209
- export { hasRequiredProps, isAlpha, isAlphanumeric, isArray, isBase64, isCreditCard, isDateInRange, isEmail, isHexColor, isIPv4, isIPv6, isISODate, isLengthBetween, isNumberBetween, isNumeric, isPhone, isPort, isStrongPassword, isURL, isUUID, isValidJSON, matchesPattern, validateAll };
235
+ export { hasRequiredProps, isAlpha, isAlphanumeric, isArray, isBase64, isCreditCard, isDateInRange, isEmail, isHexColor, isHostname, isIPv4, isIPv6, isISODate, isLengthBetween, isNumberBetween, isNumeric, isPhone, isPort, isStrongPassword, isURL, isUUID, isValidJSON, matchesPattern, validateAll };
@@ -36,13 +36,15 @@ var BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}
36
36
  var DOT_ATOM_REGEX = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*$/;
37
37
  var QUOTED_LOCAL_REGEX = /^"([\s\x21\x23-\x5B\x5D-\x7E]|\\[\x20-\x7E])*"$/;
38
38
  var TLD_REGEX = /^[A-Za-z]{2,63}$/;
39
- function isPort(value) {
40
- if (typeof value === "number") return Number.isInteger(value) && value > 0 && value <= 65535;
39
+ function isPort(value, allowZero = false) {
40
+ const minPort = allowZero ? 0 : 1;
41
+ const maxPort = 65535;
42
+ if (typeof value === "number") return Number.isInteger(value) && value >= minPort && value <= maxPort;
41
43
  if (!value || typeof value !== "string") return false;
42
44
  const str = value.trim();
43
45
  if (!PORT_REGEX.test(str)) return false;
44
46
  const port = Number(str);
45
- return Number.isInteger(port) && port > 0 && port <= 65535;
47
+ return Number.isInteger(port) && port >= minPort && port <= maxPort;
46
48
  }
47
49
  __name(isPort, "isPort");
48
50
  function isEmail(str) {
@@ -148,7 +150,7 @@ function isIPv4(str) {
148
150
  const parts = str.split(".");
149
151
  if (parts.length !== 4) return false;
150
152
  return parts.every((p) => {
151
- if (p.length > 3 || p.startsWith("0") && p.length > 1) return false;
153
+ if (!p || p.length > 3 || p.startsWith("0") && p.length > 1) return false;
152
154
  const n = Number(p);
153
155
  return Number.isInteger(n) && n >= 0 && n <= 255;
154
156
  });
@@ -163,6 +165,29 @@ function isIPv6(str) {
163
165
  }
164
166
  }
165
167
  __name(isIPv6, "isIPv6");
168
+ function isHostname(str) {
169
+ if (!str || typeof str !== "string") return false;
170
+ let input = str.trim();
171
+ if (!input || input.length > 255) return false;
172
+ if (input.endsWith(".")) {
173
+ input = input.slice(0, -1);
174
+ }
175
+ if (isIPv4(input)) return true;
176
+ if (isIPv6(input)) return true;
177
+ if (input === "localhost") return true;
178
+ if (/^\d+(\.\d+){3,}$/.test(input)) {
179
+ return false;
180
+ }
181
+ if (input.includes("..")) return false;
182
+ const labels = input.split(".");
183
+ if (labels.length === 0) return false;
184
+ const labelRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
185
+ return labels.every((label) => {
186
+ if (!label || label.length > 63) return false;
187
+ return labelRegex.test(label);
188
+ });
189
+ }
190
+ __name(isHostname, "isHostname");
166
191
  function isCreditCard(str) {
167
192
  if (!str || typeof str !== "string") return false;
168
193
  const s = str.replace(/\D/g, "");
@@ -228,4 +253,4 @@ function validateAll(value, validators) {
228
253
  }
229
254
  __name(validateAll, "validateAll");
230
255
 
231
- export { hasRequiredProps, isAlpha, isAlphanumeric, isArray, isBase64, isCreditCard, isDateInRange, isEmail, isHexColor, isIPv4, isIPv6, isISODate, isLengthBetween, isNumberBetween, isNumeric, isPhone, isPort, isStrongPassword, isURL, isUUID, isValidJSON, matchesPattern, validateAll };
256
+ export { hasRequiredProps, isAlpha, isAlphanumeric, isArray, isBase64, isCreditCard, isDateInRange, isEmail, isHexColor, isHostname, isIPv4, isIPv6, isISODate, isLengthBetween, isNumberBetween, isNumeric, isPhone, isPort, isStrongPassword, isURL, isUUID, isValidJSON, matchesPattern, validateAll };