@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/README.md +5 -2
- package/crypto/index.d.ts +1 -1
- package/date/index.cjs +343 -102
- package/date/index.d.ts +178 -6
- package/date/index.mjs +338 -103
- package/logger/index.cjs +1 -1
- package/logger/index.mjs +1 -1
- package/package.json +6 -6
- package/server/index.cjs +178 -11
- package/server/index.d.ts +89 -2
- package/server/index.mjs +180 -13
- package/types/index.d.ts +10 -2
- package/validation/index.cjs +30 -4
- package/validation/index.d.ts +29 -3
- package/validation/index.mjs +30 -5
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 (
|
|
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
|
|
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 (
|
|
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]
|
|
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 (
|
|
743
|
-
|
|
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
|
|
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 */
|
package/validation/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
package/validation/index.d.ts
CHANGED
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
/**
|
|
29
29
|
* Checks if a string is a valid port number.
|
|
30
30
|
*
|
|
31
|
-
* @param
|
|
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 };
|
package/validation/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 };
|