@optique/core 0.10.0-dev.333 → 0.10.0-dev.342

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.
@@ -649,6 +649,1844 @@ function uuid(options = {}) {
649
649
  }
650
650
  };
651
651
  }
652
+ /**
653
+ * Creates a ValueParser for TCP/UDP port numbers.
654
+ *
655
+ * This parser validates that the input is a valid port number (1-65535 by default)
656
+ * and optionally enforces range constraints and well-known port restrictions.
657
+ *
658
+ * Port numbers are validated according to the following rules:
659
+ * - Must be a valid integer (no decimals, no scientific notation)
660
+ * - Must be within the range `[min, max]` (default `[1, 65535]`)
661
+ * - If `disallowWellKnown` is `true`, ports 1-1023 are rejected
662
+ *
663
+ * The parser provides two modes of operation:
664
+ * - Regular mode: Returns JavaScript numbers (safe for all port values)
665
+ * - `bigint` mode: Returns `bigint` values for consistency with other numeric types
666
+ *
667
+ * @example
668
+ * ```typescript
669
+ * // Basic port parser (1-65535)
670
+ * option("--port", port())
671
+ *
672
+ * // Custom range (non-privileged ports only)
673
+ * option("--port", port({ min: 1024, max: 65535 }))
674
+ *
675
+ * // Disallow well-known ports (reject 1-1023)
676
+ * option("--port", port({ disallowWellKnown: true }))
677
+ *
678
+ * // Development ports only
679
+ * option("--dev-port", port({ min: 3000, max: 9000 }))
680
+ *
681
+ * // Using bigint type
682
+ * option("--port", port({ type: "bigint" }))
683
+ * ```
684
+ *
685
+ * @param options Configuration options specifying the type and constraints.
686
+ * @returns A {@link ValueParser} that converts string input to port numbers.
687
+ * @since 0.10.0
688
+ */
689
+ function port(options) {
690
+ if (options?.type === "bigint") {
691
+ const metavar$1 = options.metavar ?? "PORT";
692
+ ensureNonEmptyString(metavar$1);
693
+ const min$1 = options.min ?? 1n;
694
+ const max$1 = options.max ?? 65535n;
695
+ return {
696
+ $mode: "sync",
697
+ metavar: metavar$1,
698
+ parse(input) {
699
+ let value;
700
+ try {
701
+ value = BigInt(input);
702
+ } catch (e) {
703
+ if (e instanceof SyntaxError) return {
704
+ success: false,
705
+ error: options.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
706
+ };
707
+ throw e;
708
+ }
709
+ if (value < min$1) return {
710
+ success: false,
711
+ error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, min$1) : options.errors.belowMinimum : message`Expected a port number greater than or equal to ${text(min$1.toLocaleString("en"))}, but got ${input}.`
712
+ };
713
+ if (value > max$1) return {
714
+ success: false,
715
+ error: options.errors?.aboveMaximum ? typeof options.errors.aboveMaximum === "function" ? options.errors.aboveMaximum(value, max$1) : options.errors.aboveMaximum : message`Expected a port number less than or equal to ${text(max$1.toLocaleString("en"))}, but got ${input}.`
716
+ };
717
+ if (options.disallowWellKnown && value >= 1n && value <= 1023n) return {
718
+ success: false,
719
+ error: options.errors?.wellKnownNotAllowed ? typeof options.errors.wellKnownNotAllowed === "function" ? options.errors.wellKnownNotAllowed(value) : options.errors.wellKnownNotAllowed : message`Port ${value.toLocaleString("en")} is a well-known port (1-1023) and may require elevated privileges.`
720
+ };
721
+ return {
722
+ success: true,
723
+ value
724
+ };
725
+ },
726
+ format(value) {
727
+ return value.toString();
728
+ }
729
+ };
730
+ }
731
+ const metavar = options?.metavar ?? "PORT";
732
+ ensureNonEmptyString(metavar);
733
+ const min = options?.min ?? 1;
734
+ const max = options?.max ?? 65535;
735
+ return {
736
+ $mode: "sync",
737
+ metavar,
738
+ parse(input) {
739
+ if (!input.match(/^-?\d+$/)) return {
740
+ success: false,
741
+ error: options?.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
742
+ };
743
+ const value = Number.parseInt(input);
744
+ if (value < min) return {
745
+ success: false,
746
+ error: options?.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, min) : options.errors.belowMinimum : message`Expected a port number greater than or equal to ${text(min.toLocaleString("en"))}, but got ${input}.`
747
+ };
748
+ if (value > max) return {
749
+ success: false,
750
+ error: options?.errors?.aboveMaximum ? typeof options.errors.aboveMaximum === "function" ? options.errors.aboveMaximum(value, max) : options.errors.aboveMaximum : message`Expected a port number less than or equal to ${text(max.toLocaleString("en"))}, but got ${input}.`
751
+ };
752
+ if (options?.disallowWellKnown && value >= 1 && value <= 1023) return {
753
+ success: false,
754
+ error: options.errors?.wellKnownNotAllowed ? typeof options.errors.wellKnownNotAllowed === "function" ? options.errors.wellKnownNotAllowed(value) : options.errors.wellKnownNotAllowed : message`Port ${value.toLocaleString("en")} is a well-known port (1-1023) and may require elevated privileges.`
755
+ };
756
+ return {
757
+ success: true,
758
+ value
759
+ };
760
+ },
761
+ format(value) {
762
+ return value.toString();
763
+ }
764
+ };
765
+ }
766
+ function isPrivateIp(octets) {
767
+ if (octets[0] === 10) return true;
768
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return true;
769
+ if (octets[0] === 192 && octets[1] === 168) return true;
770
+ return false;
771
+ }
772
+ function isLoopbackIp(octets) {
773
+ return octets[0] === 127;
774
+ }
775
+ function isLinkLocalIp(octets) {
776
+ return octets[0] === 169 && octets[1] === 254;
777
+ }
778
+ function isMulticastIp(octets) {
779
+ return octets[0] >= 224 && octets[0] <= 239;
780
+ }
781
+ function isBroadcastIp(octets) {
782
+ return octets.every((o) => o === 255);
783
+ }
784
+ function isZeroIp(octets) {
785
+ return octets.every((o) => o === 0);
786
+ }
787
+ /**
788
+ * Creates a value parser for IPv4 addresses.
789
+ *
790
+ * This parser validates IPv4 addresses in dotted-decimal notation (e.g.,
791
+ * "192.168.1.1") and provides options to filter specific IP address types
792
+ * such as private, loopback, link-local, multicast, broadcast, and zero
793
+ * addresses.
794
+ *
795
+ * @param options The parser options.
796
+ * @returns A value parser for IPv4 addresses.
797
+ * @throws {TypeError} If the metavar is an empty string.
798
+ * @since 0.10.0
799
+ * @example
800
+ * ```typescript
801
+ * import { ipv4 } from "@optique/core/valueparser";
802
+ *
803
+ * // Basic IPv4 parser (allows all types)
804
+ * const address = ipv4();
805
+ *
806
+ * // Public IPs only (no private/loopback)
807
+ * const publicIp = ipv4({
808
+ * allowPrivate: false,
809
+ * allowLoopback: false
810
+ * });
811
+ *
812
+ * // Server binding (allow 0.0.0.0 and private IPs)
813
+ * const bindAddress = ipv4({
814
+ * allowZero: true,
815
+ * allowPrivate: true
816
+ * });
817
+ * ```
818
+ */
819
+ function ipv4(options) {
820
+ const metavar = options?.metavar ?? "IPV4";
821
+ ensureNonEmptyString(metavar);
822
+ const allowPrivate = options?.allowPrivate ?? true;
823
+ const allowLoopback = options?.allowLoopback ?? true;
824
+ const allowLinkLocal = options?.allowLinkLocal ?? true;
825
+ const allowMulticast = options?.allowMulticast ?? true;
826
+ const allowBroadcast = options?.allowBroadcast ?? true;
827
+ const allowZero = options?.allowZero ?? true;
828
+ return {
829
+ $mode: "sync",
830
+ metavar,
831
+ parse(input) {
832
+ const parts = input.split(".");
833
+ if (parts.length !== 4) {
834
+ const errorMsg = options?.errors?.invalidIpv4;
835
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
836
+ return {
837
+ success: false,
838
+ error: msg
839
+ };
840
+ }
841
+ const octets = [];
842
+ for (const part of parts) {
843
+ if (part.length === 0) {
844
+ const errorMsg = options?.errors?.invalidIpv4;
845
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
846
+ return {
847
+ success: false,
848
+ error: msg
849
+ };
850
+ }
851
+ if (part.trim() !== part) {
852
+ const errorMsg = options?.errors?.invalidIpv4;
853
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
854
+ return {
855
+ success: false,
856
+ error: msg
857
+ };
858
+ }
859
+ if (part.length > 1 && part[0] === "0") {
860
+ const errorMsg = options?.errors?.invalidIpv4;
861
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
862
+ return {
863
+ success: false,
864
+ error: msg
865
+ };
866
+ }
867
+ const octet = Number(part);
868
+ if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
869
+ const errorMsg = options?.errors?.invalidIpv4;
870
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
871
+ return {
872
+ success: false,
873
+ error: msg
874
+ };
875
+ }
876
+ octets.push(octet);
877
+ }
878
+ const ipAddress = octets.join(".");
879
+ if (!allowPrivate && isPrivateIp(octets)) {
880
+ const errorMsg = options?.errors?.privateNotAllowed;
881
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is a private IP address.`;
882
+ return {
883
+ success: false,
884
+ error: msg
885
+ };
886
+ }
887
+ if (!allowLoopback && isLoopbackIp(octets)) {
888
+ const errorMsg = options?.errors?.loopbackNotAllowed;
889
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is a loopback address.`;
890
+ return {
891
+ success: false,
892
+ error: msg
893
+ };
894
+ }
895
+ if (!allowLinkLocal && isLinkLocalIp(octets)) {
896
+ const errorMsg = options?.errors?.linkLocalNotAllowed;
897
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is a link-local address.`;
898
+ return {
899
+ success: false,
900
+ error: msg
901
+ };
902
+ }
903
+ if (!allowMulticast && isMulticastIp(octets)) {
904
+ const errorMsg = options?.errors?.multicastNotAllowed;
905
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is a multicast address.`;
906
+ return {
907
+ success: false,
908
+ error: msg
909
+ };
910
+ }
911
+ if (!allowBroadcast && isBroadcastIp(octets)) {
912
+ const errorMsg = options?.errors?.broadcastNotAllowed;
913
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is the broadcast address.`;
914
+ return {
915
+ success: false,
916
+ error: msg
917
+ };
918
+ }
919
+ if (!allowZero && isZeroIp(octets)) {
920
+ const errorMsg = options?.errors?.zeroNotAllowed;
921
+ const msg = typeof errorMsg === "function" ? errorMsg(ipAddress) : errorMsg ?? message`${ipAddress} is the zero address.`;
922
+ return {
923
+ success: false,
924
+ error: msg
925
+ };
926
+ }
927
+ return {
928
+ success: true,
929
+ value: ipAddress
930
+ };
931
+ },
932
+ format(value) {
933
+ return value;
934
+ }
935
+ };
936
+ }
937
+ /**
938
+ * Creates a value parser for DNS hostnames.
939
+ *
940
+ * Validates hostnames according to RFC 1123:
941
+ * - Labels separated by dots
942
+ * - Each label: 1-63 characters
943
+ * - Labels can contain alphanumeric characters and hyphens
944
+ * - Labels cannot start or end with a hyphen
945
+ * - Total length ≤ 253 characters (default)
946
+ *
947
+ * @param options - Options for hostname validation.
948
+ * @returns A value parser for hostnames.
949
+ * @since 0.10.0
950
+ *
951
+ * @example
952
+ * ```typescript
953
+ * import { hostname } from "@optique/core/valueparser";
954
+ *
955
+ * // Basic hostname parser
956
+ * const host = hostname();
957
+ *
958
+ * // Allow wildcards for certificate validation
959
+ * const domain = hostname({ allowWildcard: true });
960
+ *
961
+ * // Reject localhost
962
+ * const remoteHost = hostname({ allowLocalhost: false });
963
+ * ```
964
+ */
965
+ function hostname(options) {
966
+ const metavar = options?.metavar ?? "HOST";
967
+ ensureNonEmptyString(metavar);
968
+ const allowWildcard = options?.allowWildcard ?? false;
969
+ const allowUnderscore = options?.allowUnderscore ?? false;
970
+ const allowLocalhost = options?.allowLocalhost ?? true;
971
+ const maxLength = options?.maxLength ?? 253;
972
+ return {
973
+ $mode: "sync",
974
+ metavar,
975
+ parse(input) {
976
+ if (input.length > maxLength) {
977
+ const errorMsg = options?.errors?.tooLong;
978
+ const msg = typeof errorMsg === "function" ? errorMsg(input, maxLength) : errorMsg ?? message`Hostname ${input} is too long (maximum ${text(maxLength.toString())} characters).`;
979
+ return {
980
+ success: false,
981
+ error: msg
982
+ };
983
+ }
984
+ if (!allowLocalhost && input === "localhost") {
985
+ const errorMsg = options?.errors?.localhostNotAllowed;
986
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Hostname 'localhost' is not allowed.`;
987
+ return {
988
+ success: false,
989
+ error: msg
990
+ };
991
+ }
992
+ if (input.startsWith("*.")) {
993
+ if (!allowWildcard) {
994
+ const errorMsg = options?.errors?.wildcardNotAllowed;
995
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Wildcard hostname ${input} is not allowed.`;
996
+ return {
997
+ success: false,
998
+ error: msg
999
+ };
1000
+ }
1001
+ const rest = input.slice(2);
1002
+ if (!rest || rest.includes("*")) {
1003
+ const errorMsg = options?.errors?.invalidHostname;
1004
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1005
+ return {
1006
+ success: false,
1007
+ error: msg
1008
+ };
1009
+ }
1010
+ }
1011
+ if (!allowUnderscore && input.includes("_")) {
1012
+ const errorMsg = options?.errors?.underscoreNotAllowed;
1013
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Hostname ${input} contains underscore, which is not allowed.`;
1014
+ return {
1015
+ success: false,
1016
+ error: msg
1017
+ };
1018
+ }
1019
+ if (input.length === 0) {
1020
+ const errorMsg = options?.errors?.invalidHostname;
1021
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1022
+ return {
1023
+ success: false,
1024
+ error: msg
1025
+ };
1026
+ }
1027
+ const labels = input.split(".");
1028
+ for (const label of labels) {
1029
+ if (label.length === 0) {
1030
+ const errorMsg = options?.errors?.invalidHostname;
1031
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1032
+ return {
1033
+ success: false,
1034
+ error: msg
1035
+ };
1036
+ }
1037
+ if (label.length > 63) {
1038
+ const errorMsg = options?.errors?.invalidHostname;
1039
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1040
+ return {
1041
+ success: false,
1042
+ error: msg
1043
+ };
1044
+ }
1045
+ if (label === "*") continue;
1046
+ if (label.startsWith("-") || label.endsWith("-")) {
1047
+ const errorMsg = options?.errors?.invalidHostname;
1048
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1049
+ return {
1050
+ success: false,
1051
+ error: msg
1052
+ };
1053
+ }
1054
+ const allowedPattern = allowUnderscore ? /^[a-zA-Z0-9_-]+$/ : /^[a-zA-Z0-9-]+$/;
1055
+ if (!allowedPattern.test(label)) {
1056
+ const errorMsg = options?.errors?.invalidHostname;
1057
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid hostname, but got ${input}.`;
1058
+ return {
1059
+ success: false,
1060
+ error: msg
1061
+ };
1062
+ }
1063
+ }
1064
+ return {
1065
+ success: true,
1066
+ value: input
1067
+ };
1068
+ },
1069
+ format(value) {
1070
+ return value;
1071
+ }
1072
+ };
1073
+ }
1074
+ function email(options) {
1075
+ const metavar = options?.metavar ?? "EMAIL";
1076
+ ensureNonEmptyString(metavar);
1077
+ const allowMultiple = options?.allowMultiple ?? false;
1078
+ const allowDisplayName = options?.allowDisplayName ?? false;
1079
+ const lowercase = options?.lowercase ?? false;
1080
+ const allowedDomains = options?.allowedDomains;
1081
+ const atextRegex = /^[a-zA-Z0-9._+-]+$/;
1082
+ function validateEmail(input) {
1083
+ const trimmed = input.trim();
1084
+ let emailAddr = trimmed;
1085
+ if (allowDisplayName && trimmed.includes("<") && trimmed.endsWith(">")) {
1086
+ const match = trimmed.match(/<([^>]+)>$/);
1087
+ if (match) emailAddr = match[1].trim();
1088
+ }
1089
+ let atIndex = -1;
1090
+ if (emailAddr.startsWith("\"")) {
1091
+ const closingQuoteIndex = emailAddr.indexOf("\"", 1);
1092
+ if (closingQuoteIndex === -1) return null;
1093
+ atIndex = emailAddr.indexOf("@", closingQuoteIndex);
1094
+ } else atIndex = emailAddr.indexOf("@");
1095
+ if (atIndex === -1) return null;
1096
+ const lastAtIndex = emailAddr.lastIndexOf("@");
1097
+ if (atIndex !== lastAtIndex) return null;
1098
+ const localPart = emailAddr.substring(0, atIndex);
1099
+ const domain$1 = emailAddr.substring(atIndex + 1);
1100
+ if (!localPart || localPart.length === 0) return null;
1101
+ let isValidLocal = false;
1102
+ if (localPart.startsWith("\"") && localPart.endsWith("\"")) isValidLocal = localPart.length >= 2;
1103
+ else {
1104
+ const localParts = localPart.split(".");
1105
+ if (localPart.startsWith(".") || localPart.endsWith(".")) return null;
1106
+ isValidLocal = localParts.length > 0 && localParts.every((part) => part.length > 0 && atextRegex.test(part));
1107
+ }
1108
+ if (!isValidLocal) return null;
1109
+ if (!domain$1 || domain$1.length === 0) return null;
1110
+ if (!domain$1.includes(".")) return null;
1111
+ if (domain$1.startsWith(".") || domain$1.endsWith(".") || domain$1.startsWith("-") || domain$1.endsWith("-")) return null;
1112
+ const domainLabels = domain$1.split(".");
1113
+ for (const label of domainLabels) {
1114
+ if (label.length === 0 || label.length > 63) return null;
1115
+ if (label.startsWith("-") || label.endsWith("-")) return null;
1116
+ if (!/^[a-zA-Z0-9-]+$/.test(label)) return null;
1117
+ }
1118
+ const resultEmail = emailAddr;
1119
+ return lowercase ? resultEmail.toLowerCase() : resultEmail;
1120
+ }
1121
+ return {
1122
+ $mode: "sync",
1123
+ metavar,
1124
+ parse(input) {
1125
+ if (allowMultiple) {
1126
+ const emails = input.split(",").map((e) => e.trim());
1127
+ const validatedEmails = [];
1128
+ for (const email$1 of emails) {
1129
+ const validated = validateEmail(email$1);
1130
+ if (validated === null) {
1131
+ const errorMsg = options?.errors?.invalidEmail;
1132
+ const msg = typeof errorMsg === "function" ? errorMsg(email$1) : errorMsg ?? message`Expected a valid email address, but got ${email$1}.`;
1133
+ return {
1134
+ success: false,
1135
+ error: msg
1136
+ };
1137
+ }
1138
+ if (allowedDomains && allowedDomains.length > 0) {
1139
+ const atIndex = validated.indexOf("@");
1140
+ const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1141
+ const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
1142
+ if (!isAllowed) {
1143
+ const errorMsg = options?.errors?.domainNotAllowed;
1144
+ if (typeof errorMsg === "function") return {
1145
+ success: false,
1146
+ error: errorMsg(validated, allowedDomains)
1147
+ };
1148
+ const msg = errorMsg ?? [
1149
+ {
1150
+ type: "text",
1151
+ text: "Email domain "
1152
+ },
1153
+ {
1154
+ type: "value",
1155
+ value: domain$1
1156
+ },
1157
+ {
1158
+ type: "text",
1159
+ text: ` is not allowed. Allowed domains: ${allowedDomains.join(", ")}.`
1160
+ }
1161
+ ];
1162
+ return {
1163
+ success: false,
1164
+ error: msg
1165
+ };
1166
+ }
1167
+ }
1168
+ validatedEmails.push(validated);
1169
+ }
1170
+ return {
1171
+ success: true,
1172
+ value: validatedEmails
1173
+ };
1174
+ } else {
1175
+ const validated = validateEmail(input);
1176
+ if (validated === null) {
1177
+ const errorMsg = options?.errors?.invalidEmail;
1178
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid email address, but got ${input}.`;
1179
+ return {
1180
+ success: false,
1181
+ error: msg
1182
+ };
1183
+ }
1184
+ if (allowedDomains && allowedDomains.length > 0) {
1185
+ const atIndex = validated.indexOf("@");
1186
+ const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1187
+ const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
1188
+ if (!isAllowed) {
1189
+ const errorMsg = options?.errors?.domainNotAllowed;
1190
+ if (typeof errorMsg === "function") return {
1191
+ success: false,
1192
+ error: errorMsg(validated, allowedDomains)
1193
+ };
1194
+ const msg = errorMsg ?? [
1195
+ {
1196
+ type: "text",
1197
+ text: "Email domain "
1198
+ },
1199
+ {
1200
+ type: "value",
1201
+ value: domain$1
1202
+ },
1203
+ {
1204
+ type: "text",
1205
+ text: ` is not allowed. Allowed domains: ${allowedDomains.join(", ")}.`
1206
+ }
1207
+ ];
1208
+ return {
1209
+ success: false,
1210
+ error: msg
1211
+ };
1212
+ }
1213
+ }
1214
+ return {
1215
+ success: true,
1216
+ value: validated
1217
+ };
1218
+ }
1219
+ },
1220
+ format(value) {
1221
+ if (Array.isArray(value)) return value.join(",");
1222
+ return value;
1223
+ }
1224
+ };
1225
+ }
1226
+ /**
1227
+ * Creates a value parser for socket addresses in "host:port" format.
1228
+ *
1229
+ * Validates socket addresses with support for:
1230
+ * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
1231
+ * - Configurable host:port separator
1232
+ * - Optional default port
1233
+ * - Host type filtering (hostname only, IP only, or both)
1234
+ * - Port range validation
1235
+ *
1236
+ * @param options - Options for socket address validation.
1237
+ * @returns A value parser for socket addresses.
1238
+ * @since 0.10.0
1239
+ *
1240
+ * @example
1241
+ * ```typescript
1242
+ * import { socketAddress } from "@optique/core/valueparser";
1243
+ *
1244
+ * // Basic socket address parser
1245
+ * const endpoint = socketAddress({ requirePort: true });
1246
+ *
1247
+ * // With default port
1248
+ * const server = socketAddress({ defaultPort: 80 });
1249
+ *
1250
+ * // IP addresses only
1251
+ * const bind = socketAddress({
1252
+ * defaultPort: 8080,
1253
+ * host: { type: "ip" }
1254
+ * });
1255
+ * ```
1256
+ */
1257
+ function socketAddress(options) {
1258
+ const metavar = options?.metavar ?? "HOST:PORT";
1259
+ ensureNonEmptyString(metavar);
1260
+ const separator = options?.separator ?? ":";
1261
+ const defaultPort = options?.defaultPort;
1262
+ const requirePort = options?.requirePort ?? false;
1263
+ const hostType = options?.host?.type ?? "both";
1264
+ const hostnameParser = hostname({
1265
+ ...options?.host?.hostname,
1266
+ metavar: "HOST"
1267
+ });
1268
+ const ipParser = ipv4({
1269
+ ...options?.host?.ip,
1270
+ metavar: "HOST"
1271
+ });
1272
+ const portParser = port({
1273
+ ...options?.port,
1274
+ metavar: "PORT",
1275
+ type: "number"
1276
+ });
1277
+ function parseHost(hostInput) {
1278
+ if (hostType === "hostname") {
1279
+ const ipResult = ipParser.parse(hostInput);
1280
+ if (ipResult.success) return null;
1281
+ const result = hostnameParser.parse(hostInput);
1282
+ return result.success ? result.value : null;
1283
+ } else if (hostType === "ip") {
1284
+ const result = ipParser.parse(hostInput);
1285
+ return result.success ? result.value : null;
1286
+ } else {
1287
+ const ipResult = ipParser.parse(hostInput);
1288
+ if (ipResult.success) return ipResult.value;
1289
+ const hostnameResult = hostnameParser.parse(hostInput);
1290
+ return hostnameResult.success ? hostnameResult.value : null;
1291
+ }
1292
+ }
1293
+ return {
1294
+ $mode: "sync",
1295
+ metavar,
1296
+ parse(input) {
1297
+ const trimmed = input.trim();
1298
+ const separatorIndex = trimmed.lastIndexOf(separator);
1299
+ let hostPart;
1300
+ let portPart;
1301
+ if (separatorIndex === -1) {
1302
+ hostPart = trimmed;
1303
+ portPart = void 0;
1304
+ } else {
1305
+ hostPart = trimmed.substring(0, separatorIndex);
1306
+ portPart = trimmed.substring(separatorIndex + separator.length);
1307
+ }
1308
+ const validatedHost = parseHost(hostPart);
1309
+ if (validatedHost === null) {
1310
+ const errorMsg = options?.errors?.invalidFormat;
1311
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a socket address in format host${separator}port, but got ${input}.`;
1312
+ return {
1313
+ success: false,
1314
+ error: msg
1315
+ };
1316
+ }
1317
+ let validatedPort;
1318
+ if (portPart === void 0 || portPart === "") {
1319
+ if (requirePort) {
1320
+ const errorMsg = options?.errors?.missingPort;
1321
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Port number is required but was not specified.`;
1322
+ return {
1323
+ success: false,
1324
+ error: msg
1325
+ };
1326
+ }
1327
+ if (defaultPort !== void 0) validatedPort = defaultPort;
1328
+ else {
1329
+ const errorMsg = options?.errors?.missingPort;
1330
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Port number is required but was not specified.`;
1331
+ return {
1332
+ success: false,
1333
+ error: msg
1334
+ };
1335
+ }
1336
+ } else {
1337
+ const portResult = portParser.parse(portPart);
1338
+ if (!portResult.success) {
1339
+ const errorMsg = options?.errors?.invalidFormat;
1340
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a socket address in format host${separator}port, but got ${input}.`;
1341
+ return {
1342
+ success: false,
1343
+ error: msg
1344
+ };
1345
+ }
1346
+ validatedPort = portResult.value;
1347
+ }
1348
+ return {
1349
+ success: true,
1350
+ value: {
1351
+ host: validatedHost,
1352
+ port: validatedPort
1353
+ }
1354
+ };
1355
+ },
1356
+ format(value) {
1357
+ return `${value.host}${separator}${value.port}`;
1358
+ }
1359
+ };
1360
+ }
1361
+ function portRange(options) {
1362
+ const metavar = options?.metavar ?? "PORT-PORT";
1363
+ ensureNonEmptyString(metavar);
1364
+ const separator = options?.separator ?? "-";
1365
+ const allowSingle = options?.allowSingle ?? false;
1366
+ const isBigInt = options?.type === "bigint";
1367
+ const portParser = isBigInt ? port({
1368
+ type: "bigint",
1369
+ min: options.min,
1370
+ max: options.max,
1371
+ disallowWellKnown: options.disallowWellKnown,
1372
+ errors: options.errors
1373
+ }) : port({
1374
+ type: "number",
1375
+ min: options?.min,
1376
+ max: options?.max,
1377
+ disallowWellKnown: options?.disallowWellKnown,
1378
+ errors: options?.errors
1379
+ });
1380
+ return {
1381
+ $mode: "sync",
1382
+ metavar,
1383
+ parse(input) {
1384
+ const trimmed = input.trim();
1385
+ const separatorIndex = trimmed.indexOf(separator);
1386
+ if (separatorIndex === -1) {
1387
+ if (!allowSingle) {
1388
+ const errorMsg = options?.errors?.invalidFormat;
1389
+ if (typeof errorMsg === "function") return {
1390
+ success: false,
1391
+ error: errorMsg(input)
1392
+ };
1393
+ const msg = errorMsg ?? [
1394
+ {
1395
+ type: "text",
1396
+ text: `Expected a port range in format start${separator}end, but got `
1397
+ },
1398
+ {
1399
+ type: "value",
1400
+ value: input
1401
+ },
1402
+ {
1403
+ type: "text",
1404
+ text: "."
1405
+ }
1406
+ ];
1407
+ return {
1408
+ success: false,
1409
+ error: msg
1410
+ };
1411
+ }
1412
+ const portResult = portParser.parse(trimmed);
1413
+ if (!portResult.success) return portResult;
1414
+ const portValue = portResult.value;
1415
+ return {
1416
+ success: true,
1417
+ value: isBigInt ? {
1418
+ start: portValue,
1419
+ end: portValue
1420
+ } : {
1421
+ start: portValue,
1422
+ end: portValue
1423
+ }
1424
+ };
1425
+ }
1426
+ const startPart = trimmed.substring(0, separatorIndex);
1427
+ const endPart = trimmed.substring(separatorIndex + separator.length);
1428
+ const startResult = portParser.parse(startPart);
1429
+ if (!startResult.success) return startResult;
1430
+ const endResult = portParser.parse(endPart);
1431
+ if (!endResult.success) return endResult;
1432
+ const startValue = startResult.value;
1433
+ const endValue = endResult.value;
1434
+ if (isBigInt) {
1435
+ const start = startValue;
1436
+ const end = endValue;
1437
+ if (start > end) {
1438
+ const errorMsg = options.errors?.invalidRange;
1439
+ const msg = typeof errorMsg === "function" ? errorMsg(start, end) : errorMsg ?? message`Start port ${startPart} must be less than or equal to end port ${endPart}.`;
1440
+ return {
1441
+ success: false,
1442
+ error: msg
1443
+ };
1444
+ }
1445
+ return {
1446
+ success: true,
1447
+ value: {
1448
+ start,
1449
+ end
1450
+ }
1451
+ };
1452
+ } else {
1453
+ const start = startValue;
1454
+ const end = endValue;
1455
+ if (start > end) {
1456
+ const errorMsg = options?.errors?.invalidRange;
1457
+ const msg = typeof errorMsg === "function" ? errorMsg(start, end) : errorMsg ?? message`Start port ${startPart} must be less than or equal to end port ${endPart}.`;
1458
+ return {
1459
+ success: false,
1460
+ error: msg
1461
+ };
1462
+ }
1463
+ return {
1464
+ success: true,
1465
+ value: {
1466
+ start,
1467
+ end
1468
+ }
1469
+ };
1470
+ }
1471
+ },
1472
+ format(value) {
1473
+ return `${value.start}${separator}${value.end}`;
1474
+ }
1475
+ };
1476
+ }
1477
+ /**
1478
+ * Creates a value parser for MAC (Media Access Control) addresses.
1479
+ *
1480
+ * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1481
+ * - Colon-separated: `00:1A:2B:3C:4D:5E`
1482
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1483
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1484
+ * - No separator: `001A2B3C4D5E`
1485
+ *
1486
+ * Returns the MAC address as a formatted string according to `case` and
1487
+ * `outputSeparator` options.
1488
+ *
1489
+ * @param options Configuration options for the MAC address parser.
1490
+ * @returns A parser that validates MAC addresses and returns formatted strings.
1491
+ * @since 0.10.0
1492
+ *
1493
+ * @example
1494
+ * ```typescript
1495
+ * import { macAddress } from "@optique/core/valueparser";
1496
+ *
1497
+ * // Accept any format
1498
+ * const mac = macAddress();
1499
+ *
1500
+ * // Normalize to uppercase colon-separated
1501
+ * const normalizedMac = macAddress({
1502
+ * outputSeparator: ":",
1503
+ * case: "upper"
1504
+ * });
1505
+ * ```
1506
+ */
1507
+ function macAddress(options) {
1508
+ const separator = options?.separator ?? "any";
1509
+ const caseOption = options?.case ?? "preserve";
1510
+ const outputSeparator = options?.outputSeparator;
1511
+ const metavar = options?.metavar ?? "MAC";
1512
+ const colonRegex = /^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/;
1513
+ const hyphenRegex = /^([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})$/;
1514
+ const dotRegex = /^([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})$/;
1515
+ const noneRegex = /^([0-9a-fA-F]{12})$/;
1516
+ return {
1517
+ $mode: "sync",
1518
+ metavar,
1519
+ parse(input) {
1520
+ let octets = [];
1521
+ let inputSeparator;
1522
+ if (separator === ":" || separator === "any") {
1523
+ const match = colonRegex.exec(input);
1524
+ if (match) {
1525
+ octets = match.slice(1, 7);
1526
+ inputSeparator = ":";
1527
+ }
1528
+ }
1529
+ if (octets.length === 0 && (separator === "-" || separator === "any")) {
1530
+ const match = hyphenRegex.exec(input);
1531
+ if (match) {
1532
+ octets = match.slice(1, 7);
1533
+ inputSeparator = "-";
1534
+ }
1535
+ }
1536
+ if (octets.length === 0 && (separator === "." || separator === "any")) {
1537
+ const match = dotRegex.exec(input);
1538
+ if (match) {
1539
+ const groups = match.slice(1, 4);
1540
+ octets = groups.flatMap((group) => [group.slice(0, 2), group.slice(2, 4)]);
1541
+ inputSeparator = ".";
1542
+ }
1543
+ }
1544
+ if (octets.length === 0 && (separator === "none" || separator === "any")) {
1545
+ const match = noneRegex.exec(input);
1546
+ if (match) {
1547
+ const hex = match[1];
1548
+ octets = [];
1549
+ for (let i = 0; i < 12; i += 2) octets.push(hex.slice(i, i + 2));
1550
+ inputSeparator = "none";
1551
+ }
1552
+ }
1553
+ if (octets.length === 0) {
1554
+ const errorMsg = options?.errors?.invalidMacAddress;
1555
+ if (typeof errorMsg === "function") return {
1556
+ success: false,
1557
+ error: errorMsg(input)
1558
+ };
1559
+ const msg = errorMsg ?? [
1560
+ {
1561
+ type: "text",
1562
+ text: "Expected a valid MAC address, but got "
1563
+ },
1564
+ {
1565
+ type: "value",
1566
+ value: input
1567
+ },
1568
+ {
1569
+ type: "text",
1570
+ text: "."
1571
+ }
1572
+ ];
1573
+ return {
1574
+ success: false,
1575
+ error: msg
1576
+ };
1577
+ }
1578
+ let formattedOctets = octets;
1579
+ if (caseOption === "upper") formattedOctets = octets.map((octet) => octet.toUpperCase());
1580
+ else if (caseOption === "lower") formattedOctets = octets.map((octet) => octet.toLowerCase());
1581
+ const finalSeparator = outputSeparator ?? inputSeparator ?? ":";
1582
+ let result;
1583
+ if (finalSeparator === ":") result = formattedOctets.join(":");
1584
+ else if (finalSeparator === "-") result = formattedOctets.join("-");
1585
+ else if (finalSeparator === ".") result = [
1586
+ formattedOctets[0] + formattedOctets[1],
1587
+ formattedOctets[2] + formattedOctets[3],
1588
+ formattedOctets[4] + formattedOctets[5]
1589
+ ].join(".");
1590
+ else result = formattedOctets.join("");
1591
+ return {
1592
+ success: true,
1593
+ value: result
1594
+ };
1595
+ },
1596
+ format() {
1597
+ return metavar;
1598
+ }
1599
+ };
1600
+ }
1601
+ /**
1602
+ * Creates a value parser for domain names.
1603
+ *
1604
+ * Validates domain names according to RFC 1035 with configurable options for
1605
+ * subdomain filtering, TLD restrictions, minimum label requirements, and case
1606
+ * normalization.
1607
+ *
1608
+ * @param options Parser options for domain validation.
1609
+ * @returns A parser that accepts valid domain names as strings.
1610
+ *
1611
+ * @example
1612
+ * ``` typescript
1613
+ * import { option } from "@optique/core/primitives";
1614
+ * import { domain } from "@optique/core/valueparser";
1615
+ *
1616
+ * // Accept any valid domain
1617
+ * option("--domain", domain())
1618
+ *
1619
+ * // Root domains only (no subdomains)
1620
+ * option("--root", domain({ allowSubdomains: false }))
1621
+ *
1622
+ * // Restrict to specific TLDs
1623
+ * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1624
+ *
1625
+ * // Normalize to lowercase
1626
+ * option("--domain", domain({ lowercase: true }))
1627
+ * ```
1628
+ *
1629
+ * @since 0.10.0
1630
+ */
1631
+ function domain(options) {
1632
+ const metavar = options?.metavar ?? "DOMAIN";
1633
+ const allowSubdomains = options?.allowSubdomains ?? true;
1634
+ const allowedTLDs = options?.allowedTLDs;
1635
+ const minLabels = options?.minLabels ?? 2;
1636
+ const lowercase = options?.lowercase ?? false;
1637
+ const errors = options?.errors;
1638
+ const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1639
+ return {
1640
+ $mode: "sync",
1641
+ metavar,
1642
+ parse(input) {
1643
+ if (input.length === 0 || input.startsWith(".") || input.endsWith(".")) {
1644
+ const errorMsg = errors?.invalidDomain;
1645
+ if (typeof errorMsg === "function") return {
1646
+ success: false,
1647
+ error: errorMsg(input)
1648
+ };
1649
+ const msg = errorMsg ?? [
1650
+ {
1651
+ type: "text",
1652
+ text: "Expected a valid domain name, but got "
1653
+ },
1654
+ {
1655
+ type: "value",
1656
+ value: input
1657
+ },
1658
+ {
1659
+ type: "text",
1660
+ text: "."
1661
+ }
1662
+ ];
1663
+ return {
1664
+ success: false,
1665
+ error: msg
1666
+ };
1667
+ }
1668
+ if (input.includes("..")) {
1669
+ const errorMsg = errors?.invalidDomain;
1670
+ if (typeof errorMsg === "function") return {
1671
+ success: false,
1672
+ error: errorMsg(input)
1673
+ };
1674
+ const msg = errorMsg ?? [
1675
+ {
1676
+ type: "text",
1677
+ text: "Expected a valid domain name, but got "
1678
+ },
1679
+ {
1680
+ type: "value",
1681
+ value: input
1682
+ },
1683
+ {
1684
+ type: "text",
1685
+ text: "."
1686
+ }
1687
+ ];
1688
+ return {
1689
+ success: false,
1690
+ error: msg
1691
+ };
1692
+ }
1693
+ const labels = input.split(".");
1694
+ for (const label of labels) if (!labelRegex.test(label)) {
1695
+ const errorMsg = errors?.invalidDomain;
1696
+ if (typeof errorMsg === "function") return {
1697
+ success: false,
1698
+ error: errorMsg(input)
1699
+ };
1700
+ const msg = errorMsg ?? [
1701
+ {
1702
+ type: "text",
1703
+ text: "Expected a valid domain name, but got "
1704
+ },
1705
+ {
1706
+ type: "value",
1707
+ value: input
1708
+ },
1709
+ {
1710
+ type: "text",
1711
+ text: "."
1712
+ }
1713
+ ];
1714
+ return {
1715
+ success: false,
1716
+ error: msg
1717
+ };
1718
+ }
1719
+ if (labels.length < minLabels) {
1720
+ const errorMsg = errors?.tooFewLabels;
1721
+ if (typeof errorMsg === "function") return {
1722
+ success: false,
1723
+ error: errorMsg(input, minLabels)
1724
+ };
1725
+ const msg = errorMsg ?? [
1726
+ {
1727
+ type: "text",
1728
+ text: "Domain "
1729
+ },
1730
+ {
1731
+ type: "value",
1732
+ value: input
1733
+ },
1734
+ {
1735
+ type: "text",
1736
+ text: ` must have at least ${minLabels} labels.`
1737
+ }
1738
+ ];
1739
+ return {
1740
+ success: false,
1741
+ error: msg
1742
+ };
1743
+ }
1744
+ if (!allowSubdomains && labels.length > 2) {
1745
+ const errorMsg = errors?.subdomainsNotAllowed;
1746
+ if (typeof errorMsg === "function") return {
1747
+ success: false,
1748
+ error: errorMsg(input)
1749
+ };
1750
+ const msg = errorMsg ?? [
1751
+ {
1752
+ type: "text",
1753
+ text: "Subdomains are not allowed, but got "
1754
+ },
1755
+ {
1756
+ type: "value",
1757
+ value: input
1758
+ },
1759
+ {
1760
+ type: "text",
1761
+ text: "."
1762
+ }
1763
+ ];
1764
+ return {
1765
+ success: false,
1766
+ error: msg
1767
+ };
1768
+ }
1769
+ if (allowedTLDs !== void 0) {
1770
+ const tld = labels[labels.length - 1];
1771
+ const tldLower = tld.toLowerCase();
1772
+ const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
1773
+ if (!allowedTLDsLower.includes(tldLower)) {
1774
+ const errorMsg = errors?.tldNotAllowed;
1775
+ if (typeof errorMsg === "function") return {
1776
+ success: false,
1777
+ error: errorMsg(tld, allowedTLDs)
1778
+ };
1779
+ const msg = errorMsg ?? [
1780
+ {
1781
+ type: "text",
1782
+ text: "Top-level domain "
1783
+ },
1784
+ {
1785
+ type: "value",
1786
+ value: tld
1787
+ },
1788
+ {
1789
+ type: "text",
1790
+ text: ` is not allowed. Allowed TLDs: ${allowedTLDs.join(", ")}.`
1791
+ }
1792
+ ];
1793
+ return {
1794
+ success: false,
1795
+ error: msg
1796
+ };
1797
+ }
1798
+ }
1799
+ const result = lowercase ? input.toLowerCase() : input;
1800
+ return {
1801
+ success: true,
1802
+ value: result
1803
+ };
1804
+ },
1805
+ format() {
1806
+ return metavar;
1807
+ }
1808
+ };
1809
+ }
1810
+ /**
1811
+ * Creates a value parser for IPv6 addresses.
1812
+ *
1813
+ * Validates and normalizes IPv6 addresses to canonical form (lowercase,
1814
+ * compressed using `::` notation where appropriate).
1815
+ *
1816
+ * @param options Configuration options for IPv6 validation.
1817
+ * @returns A value parser that validates IPv6 addresses.
1818
+ *
1819
+ * @example
1820
+ * ```typescript
1821
+ * // Basic IPv6 parser
1822
+ * option("--ipv6", ipv6())
1823
+ *
1824
+ * // Global unicast only (no link-local, no unique local)
1825
+ * option("--public-ipv6", ipv6({
1826
+ * allowLinkLocal: false,
1827
+ * allowUniqueLocal: false
1828
+ * }))
1829
+ * ```
1830
+ *
1831
+ * @since 0.10.0
1832
+ */
1833
+ function ipv6(options) {
1834
+ const allowLoopback = options?.allowLoopback ?? true;
1835
+ const allowLinkLocal = options?.allowLinkLocal ?? true;
1836
+ const allowUniqueLocal = options?.allowUniqueLocal ?? true;
1837
+ const allowMulticast = options?.allowMulticast ?? true;
1838
+ const allowZero = options?.allowZero ?? true;
1839
+ const errors = options?.errors;
1840
+ const metavar = options?.metavar ?? "IPV6";
1841
+ return {
1842
+ $mode: "sync",
1843
+ metavar,
1844
+ parse(input) {
1845
+ const normalized = parseAndNormalizeIpv6(input);
1846
+ if (normalized === null) {
1847
+ const errorMsg = errors?.invalidIpv6;
1848
+ if (typeof errorMsg === "function") return {
1849
+ success: false,
1850
+ error: errorMsg(input)
1851
+ };
1852
+ const msg = errorMsg ?? [
1853
+ {
1854
+ type: "text",
1855
+ text: "Expected a valid IPv6 address, but got "
1856
+ },
1857
+ {
1858
+ type: "value",
1859
+ value: input
1860
+ },
1861
+ {
1862
+ type: "text",
1863
+ text: "."
1864
+ }
1865
+ ];
1866
+ return {
1867
+ success: false,
1868
+ error: msg
1869
+ };
1870
+ }
1871
+ if (!allowZero && normalized === "::") {
1872
+ const errorMsg = errors?.zeroNotAllowed;
1873
+ if (typeof errorMsg === "function") return {
1874
+ success: false,
1875
+ error: errorMsg(normalized)
1876
+ };
1877
+ const msg = errorMsg ?? [{
1878
+ type: "value",
1879
+ value: normalized
1880
+ }, {
1881
+ type: "text",
1882
+ text: " is the zero address."
1883
+ }];
1884
+ return {
1885
+ success: false,
1886
+ error: msg
1887
+ };
1888
+ }
1889
+ if (!allowLoopback && normalized === "::1") {
1890
+ const errorMsg = errors?.loopbackNotAllowed;
1891
+ if (typeof errorMsg === "function") return {
1892
+ success: false,
1893
+ error: errorMsg(normalized)
1894
+ };
1895
+ const msg = errorMsg ?? [{
1896
+ type: "value",
1897
+ value: normalized
1898
+ }, {
1899
+ type: "text",
1900
+ text: " is a loopback address."
1901
+ }];
1902
+ return {
1903
+ success: false,
1904
+ error: msg
1905
+ };
1906
+ }
1907
+ const groups = expandIpv6(normalized);
1908
+ if (groups === null) {
1909
+ const errorMsg = errors?.invalidIpv6;
1910
+ if (typeof errorMsg === "function") return {
1911
+ success: false,
1912
+ error: errorMsg(input)
1913
+ };
1914
+ const msg = errorMsg ?? [
1915
+ {
1916
+ type: "text",
1917
+ text: "Expected a valid IPv6 address, but got "
1918
+ },
1919
+ {
1920
+ type: "value",
1921
+ value: input
1922
+ },
1923
+ {
1924
+ type: "text",
1925
+ text: "."
1926
+ }
1927
+ ];
1928
+ return {
1929
+ success: false,
1930
+ error: msg
1931
+ };
1932
+ }
1933
+ const firstGroup = parseInt(groups[0], 16);
1934
+ if (!allowLinkLocal && (firstGroup & 65472) === 65152) {
1935
+ const errorMsg = errors?.linkLocalNotAllowed;
1936
+ if (typeof errorMsg === "function") return {
1937
+ success: false,
1938
+ error: errorMsg(normalized)
1939
+ };
1940
+ const msg = errorMsg ?? [{
1941
+ type: "value",
1942
+ value: normalized
1943
+ }, {
1944
+ type: "text",
1945
+ text: " is a link-local address."
1946
+ }];
1947
+ return {
1948
+ success: false,
1949
+ error: msg
1950
+ };
1951
+ }
1952
+ if (!allowUniqueLocal && (firstGroup & 65024) === 64512) {
1953
+ const errorMsg = errors?.uniqueLocalNotAllowed;
1954
+ if (typeof errorMsg === "function") return {
1955
+ success: false,
1956
+ error: errorMsg(normalized)
1957
+ };
1958
+ const msg = errorMsg ?? [{
1959
+ type: "value",
1960
+ value: normalized
1961
+ }, {
1962
+ type: "text",
1963
+ text: " is a unique local address."
1964
+ }];
1965
+ return {
1966
+ success: false,
1967
+ error: msg
1968
+ };
1969
+ }
1970
+ if (!allowMulticast && (firstGroup & 65280) === 65280) {
1971
+ const errorMsg = errors?.multicastNotAllowed;
1972
+ if (typeof errorMsg === "function") return {
1973
+ success: false,
1974
+ error: errorMsg(normalized)
1975
+ };
1976
+ const msg = errorMsg ?? [{
1977
+ type: "value",
1978
+ value: normalized
1979
+ }, {
1980
+ type: "text",
1981
+ text: " is a multicast address."
1982
+ }];
1983
+ return {
1984
+ success: false,
1985
+ error: msg
1986
+ };
1987
+ }
1988
+ return {
1989
+ success: true,
1990
+ value: normalized
1991
+ };
1992
+ },
1993
+ format() {
1994
+ return metavar;
1995
+ }
1996
+ };
1997
+ }
1998
+ /**
1999
+ * Parses and normalizes an IPv6 address to canonical form.
2000
+ * Returns null if the input is not a valid IPv6 address.
2001
+ */
2002
+ function parseAndNormalizeIpv6(input) {
2003
+ if (input.length === 0) return null;
2004
+ const ipv4MappedMatch = input.match(/^(.+):(\d+\.\d+\.\d+\.\d+)$/);
2005
+ if (ipv4MappedMatch) {
2006
+ const ipv6Part = ipv4MappedMatch[1];
2007
+ const ipv4Part = ipv4MappedMatch[2];
2008
+ const ipv4Octets = ipv4Part.split(".");
2009
+ if (ipv4Octets.length !== 4) return null;
2010
+ const octets = ipv4Octets.map((o) => parseInt(o, 10));
2011
+ if (octets.some((o) => isNaN(o) || o < 0 || o > 255)) return null;
2012
+ const group1 = octets[0] << 8 | octets[1];
2013
+ const group2 = octets[2] << 8 | octets[3];
2014
+ const fullAddress = `${ipv6Part}:${group1.toString(16)}:${group2.toString(16)}`;
2015
+ return parseAndNormalizeIpv6(fullAddress);
2016
+ }
2017
+ const compressionCount = (input.match(/::/g) || []).length;
2018
+ if (compressionCount > 1) return null;
2019
+ let groups;
2020
+ if (input.includes("::")) {
2021
+ const parts = input.split("::");
2022
+ if (parts.length > 2) return null;
2023
+ const leftGroups = parts[0] ? parts[0].split(":") : [];
2024
+ const rightGroups = parts[1] ? parts[1].split(":") : [];
2025
+ const totalGroups = leftGroups.length + rightGroups.length;
2026
+ if (totalGroups > 8) return null;
2027
+ const zeroCount = 8 - totalGroups;
2028
+ const zeros = Array(zeroCount).fill("0");
2029
+ groups = [
2030
+ ...leftGroups,
2031
+ ...zeros,
2032
+ ...rightGroups
2033
+ ];
2034
+ } else {
2035
+ groups = input.split(":");
2036
+ if (groups.length !== 8) return null;
2037
+ }
2038
+ for (let i = 0; i < groups.length; i++) {
2039
+ const group = groups[i];
2040
+ if (group.length === 0 || group.length > 4) return null;
2041
+ if (!/^[0-9a-fA-F]+$/.test(group)) return null;
2042
+ groups[i] = parseInt(group, 16).toString(16);
2043
+ }
2044
+ return compressIpv6(groups);
2045
+ }
2046
+ /**
2047
+ * Expands a compressed IPv6 address to 8 groups of 4 hex digits.
2048
+ * Returns null if the input is invalid.
2049
+ */
2050
+ function expandIpv6(input) {
2051
+ if (input.includes("::")) {
2052
+ const parts = input.split("::");
2053
+ if (parts.length > 2) return null;
2054
+ const leftGroups = parts[0] ? parts[0].split(":").filter((g) => g) : [];
2055
+ const rightGroups = parts[1] ? parts[1].split(":").filter((g) => g) : [];
2056
+ const totalGroups = leftGroups.length + rightGroups.length;
2057
+ if (totalGroups > 8) return null;
2058
+ const zeroCount = 8 - totalGroups;
2059
+ const zeros = Array(zeroCount).fill("0");
2060
+ const groups = [
2061
+ ...leftGroups,
2062
+ ...zeros,
2063
+ ...rightGroups
2064
+ ];
2065
+ return groups.map((g) => g.padStart(4, "0"));
2066
+ } else {
2067
+ const groups = input.split(":");
2068
+ if (groups.length !== 8) return null;
2069
+ return groups.map((g) => g.padStart(4, "0"));
2070
+ }
2071
+ }
2072
+ /**
2073
+ * Compresses an IPv6 address by replacing the longest sequence of zeros with ::.
2074
+ */
2075
+ function compressIpv6(groups) {
2076
+ let longestStart = -1;
2077
+ let longestLength = 0;
2078
+ let currentStart = -1;
2079
+ let currentLength = 0;
2080
+ for (let i = 0; i < groups.length; i++) if (groups[i] === "0") if (currentStart === -1) {
2081
+ currentStart = i;
2082
+ currentLength = 1;
2083
+ } else currentLength++;
2084
+ else {
2085
+ if (currentLength > longestLength) {
2086
+ longestStart = currentStart;
2087
+ longestLength = currentLength;
2088
+ }
2089
+ currentStart = -1;
2090
+ currentLength = 0;
2091
+ }
2092
+ if (currentLength > longestLength) {
2093
+ longestStart = currentStart;
2094
+ longestLength = currentLength;
2095
+ }
2096
+ if (longestLength < 2) return groups.join(":");
2097
+ const before = groups.slice(0, longestStart);
2098
+ const after = groups.slice(longestStart + longestLength);
2099
+ if (before.length === 0 && after.length === 0) return "::";
2100
+ else if (before.length === 0) return "::" + after.join(":");
2101
+ else if (after.length === 0) return before.join(":") + "::";
2102
+ else return before.join(":") + "::" + after.join(":");
2103
+ }
2104
+ /**
2105
+ * Creates a value parser that accepts both IPv4 and IPv6 addresses.
2106
+ *
2107
+ * By default, accepts both IPv4 and IPv6 addresses. Use the `version` option
2108
+ * to restrict to a specific IP version.
2109
+ *
2110
+ * @param options Configuration options for IP validation.
2111
+ * @returns A value parser that validates IP addresses.
2112
+ *
2113
+ * @example
2114
+ * ```typescript
2115
+ * // Accept both IPv4 and IPv6
2116
+ * option("--ip", ip())
2117
+ *
2118
+ * // IPv4 only
2119
+ * option("--ipv4", ip({ version: 4 }))
2120
+ *
2121
+ * // Public IPs only (both versions)
2122
+ * option("--public-ip", ip({
2123
+ * ipv4: { allowPrivate: false, allowLoopback: false },
2124
+ * ipv6: { allowLinkLocal: false, allowUniqueLocal: false }
2125
+ * }))
2126
+ * ```
2127
+ *
2128
+ * @since 0.10.0
2129
+ */
2130
+ function ip(options) {
2131
+ const version = options?.version ?? "both";
2132
+ const metavar = options?.metavar ?? "IP";
2133
+ const errors = options?.errors;
2134
+ const ipv4Parser = version === 4 || version === "both" ? ipv4({
2135
+ ...options?.ipv4,
2136
+ errors: {
2137
+ invalidIpv4: errors?.invalidIP,
2138
+ privateNotAllowed: errors?.privateNotAllowed,
2139
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
2140
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
2141
+ multicastNotAllowed: errors?.multicastNotAllowed,
2142
+ broadcastNotAllowed: errors?.broadcastNotAllowed,
2143
+ zeroNotAllowed: errors?.zeroNotAllowed
2144
+ }
2145
+ }) : null;
2146
+ const ipv6Parser = version === 6 || version === "both" ? ipv6({
2147
+ ...options?.ipv6,
2148
+ errors: {
2149
+ invalidIpv6: errors?.invalidIP,
2150
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
2151
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
2152
+ uniqueLocalNotAllowed: errors?.uniqueLocalNotAllowed,
2153
+ multicastNotAllowed: errors?.multicastNotAllowed,
2154
+ zeroNotAllowed: errors?.zeroNotAllowed
2155
+ }
2156
+ }) : null;
2157
+ return {
2158
+ $mode: "sync",
2159
+ metavar,
2160
+ parse(input) {
2161
+ let ipv4Error = null;
2162
+ let ipv6Error = null;
2163
+ if (ipv4Parser !== null) {
2164
+ const result = ipv4Parser.parse(input);
2165
+ if (result.success) return result;
2166
+ ipv4Error = result;
2167
+ if (version === 4) return result;
2168
+ }
2169
+ if (ipv6Parser !== null) {
2170
+ const result = ipv6Parser.parse(input);
2171
+ if (result.success) return result;
2172
+ ipv6Error = result;
2173
+ if (version === 6) return result;
2174
+ }
2175
+ if (ipv4Error !== null && !ipv4Error.success) {
2176
+ const isGeneric = ipv4Error.error.some((term) => term.type === "text" && term.text.includes("Expected"));
2177
+ if (!isGeneric) return ipv4Error;
2178
+ }
2179
+ if (ipv6Error !== null && !ipv6Error.success) {
2180
+ const isGeneric = ipv6Error.error.some((term) => term.type === "text" && term.text.includes("Expected"));
2181
+ if (!isGeneric) return ipv6Error;
2182
+ }
2183
+ const errorMsg = errors?.invalidIP;
2184
+ if (typeof errorMsg === "function") return {
2185
+ success: false,
2186
+ error: errorMsg(input)
2187
+ };
2188
+ const msg = errorMsg ?? [
2189
+ {
2190
+ type: "text",
2191
+ text: "Expected a valid IP address, but got "
2192
+ },
2193
+ {
2194
+ type: "value",
2195
+ value: input
2196
+ },
2197
+ {
2198
+ type: "text",
2199
+ text: "."
2200
+ }
2201
+ ];
2202
+ return {
2203
+ success: false,
2204
+ error: msg
2205
+ };
2206
+ },
2207
+ format() {
2208
+ return metavar;
2209
+ }
2210
+ };
2211
+ }
2212
+ /**
2213
+ * Creates a value parser for CIDR notation (IP address with prefix length).
2214
+ *
2215
+ * Parses and validates CIDR notation like `192.168.0.0/24` or `2001:db8::/32`.
2216
+ * Returns a structured object with the normalized IP address, prefix length,
2217
+ * and IP version.
2218
+ *
2219
+ * @param options Configuration options for CIDR validation.
2220
+ * @returns A value parser that validates CIDR notation.
2221
+ *
2222
+ * @example
2223
+ * ```typescript
2224
+ * // Accept both IPv4 and IPv6 CIDR
2225
+ * option("--network", cidr())
2226
+ *
2227
+ * // IPv4 CIDR only with prefix constraints
2228
+ * option("--subnet", cidr({
2229
+ * version: 4,
2230
+ * minPrefix: 16,
2231
+ * maxPrefix: 24
2232
+ * }))
2233
+ * ```
2234
+ *
2235
+ * @since 0.10.0
2236
+ */
2237
+ function cidr(options) {
2238
+ const version = options?.version ?? "both";
2239
+ const minPrefix = options?.minPrefix;
2240
+ const maxPrefix = options?.maxPrefix;
2241
+ const errors = options?.errors;
2242
+ const metavar = options?.metavar ?? "CIDR";
2243
+ const ipv4Parser = version === 4 || version === "both" ? ipv4(options?.ipv4) : null;
2244
+ const ipv6Parser = version === 6 || version === "both" ? ipv6(options?.ipv6) : null;
2245
+ return {
2246
+ $mode: "sync",
2247
+ metavar,
2248
+ parse(input) {
2249
+ const slashIndex = input.lastIndexOf("/");
2250
+ if (slashIndex === -1) {
2251
+ const errorMsg = errors?.invalidCidr;
2252
+ if (typeof errorMsg === "function") return {
2253
+ success: false,
2254
+ error: errorMsg(input)
2255
+ };
2256
+ const msg = errorMsg ?? [
2257
+ {
2258
+ type: "text",
2259
+ text: "Expected a valid CIDR notation, but got "
2260
+ },
2261
+ {
2262
+ type: "value",
2263
+ value: input
2264
+ },
2265
+ {
2266
+ type: "text",
2267
+ text: "."
2268
+ }
2269
+ ];
2270
+ return {
2271
+ success: false,
2272
+ error: msg
2273
+ };
2274
+ }
2275
+ const ipPart = input.slice(0, slashIndex);
2276
+ const prefixPart = input.slice(slashIndex + 1);
2277
+ const prefix = parseInt(prefixPart, 10);
2278
+ if (!Number.isInteger(prefix) || prefixPart !== prefix.toString() || prefix < 0) {
2279
+ const errorMsg = errors?.invalidCidr;
2280
+ if (typeof errorMsg === "function") return {
2281
+ success: false,
2282
+ error: errorMsg(input)
2283
+ };
2284
+ const msg = errorMsg ?? [
2285
+ {
2286
+ type: "text",
2287
+ text: "Expected a valid CIDR notation, but got "
2288
+ },
2289
+ {
2290
+ type: "value",
2291
+ value: input
2292
+ },
2293
+ {
2294
+ type: "text",
2295
+ text: "."
2296
+ }
2297
+ ];
2298
+ return {
2299
+ success: false,
2300
+ error: msg
2301
+ };
2302
+ }
2303
+ let ipVersion = null;
2304
+ let normalizedIp = null;
2305
+ if (ipv4Parser !== null) {
2306
+ const result = ipv4Parser.parse(ipPart);
2307
+ if (result.success) {
2308
+ ipVersion = 4;
2309
+ normalizedIp = result.value;
2310
+ if (prefix > 32) {
2311
+ const errorMsg = errors?.invalidPrefix;
2312
+ if (typeof errorMsg === "function") return {
2313
+ success: false,
2314
+ error: errorMsg(prefix, 4)
2315
+ };
2316
+ const msg = errorMsg ?? [
2317
+ {
2318
+ type: "text",
2319
+ text: "Expected a prefix length between 0 and "
2320
+ },
2321
+ {
2322
+ type: "text",
2323
+ text: "32"
2324
+ },
2325
+ {
2326
+ type: "text",
2327
+ text: " for IPv4, but got "
2328
+ },
2329
+ {
2330
+ type: "text",
2331
+ text: prefix.toString()
2332
+ },
2333
+ {
2334
+ type: "text",
2335
+ text: "."
2336
+ }
2337
+ ];
2338
+ return {
2339
+ success: false,
2340
+ error: msg
2341
+ };
2342
+ }
2343
+ }
2344
+ }
2345
+ if (ipVersion === null && ipv6Parser !== null) {
2346
+ const result = ipv6Parser.parse(ipPart);
2347
+ if (result.success) {
2348
+ ipVersion = 6;
2349
+ normalizedIp = result.value;
2350
+ if (prefix > 128) {
2351
+ const errorMsg = errors?.invalidPrefix;
2352
+ if (typeof errorMsg === "function") return {
2353
+ success: false,
2354
+ error: errorMsg(prefix, 6)
2355
+ };
2356
+ const msg = errorMsg ?? [
2357
+ {
2358
+ type: "text",
2359
+ text: "Expected a prefix length between 0 and "
2360
+ },
2361
+ {
2362
+ type: "text",
2363
+ text: "128"
2364
+ },
2365
+ {
2366
+ type: "text",
2367
+ text: " for IPv6, but got "
2368
+ },
2369
+ {
2370
+ type: "text",
2371
+ text: prefix.toString()
2372
+ },
2373
+ {
2374
+ type: "text",
2375
+ text: "."
2376
+ }
2377
+ ];
2378
+ return {
2379
+ success: false,
2380
+ error: msg
2381
+ };
2382
+ }
2383
+ }
2384
+ }
2385
+ if (ipVersion === null || normalizedIp === null) {
2386
+ const errorMsg = errors?.invalidCidr;
2387
+ if (typeof errorMsg === "function") return {
2388
+ success: false,
2389
+ error: errorMsg(input)
2390
+ };
2391
+ const msg = errorMsg ?? [
2392
+ {
2393
+ type: "text",
2394
+ text: "Expected a valid CIDR notation, but got "
2395
+ },
2396
+ {
2397
+ type: "value",
2398
+ value: input
2399
+ },
2400
+ {
2401
+ type: "text",
2402
+ text: "."
2403
+ }
2404
+ ];
2405
+ return {
2406
+ success: false,
2407
+ error: msg
2408
+ };
2409
+ }
2410
+ if (minPrefix !== void 0 && prefix < minPrefix) {
2411
+ const errorMsg = errors?.prefixBelowMinimum;
2412
+ if (typeof errorMsg === "function") return {
2413
+ success: false,
2414
+ error: errorMsg(prefix, minPrefix)
2415
+ };
2416
+ const msg = errorMsg ?? [
2417
+ {
2418
+ type: "text",
2419
+ text: "Expected a prefix length greater than or equal to "
2420
+ },
2421
+ {
2422
+ type: "text",
2423
+ text: minPrefix.toString()
2424
+ },
2425
+ {
2426
+ type: "text",
2427
+ text: ", but got "
2428
+ },
2429
+ {
2430
+ type: "text",
2431
+ text: prefix.toString()
2432
+ },
2433
+ {
2434
+ type: "text",
2435
+ text: "."
2436
+ }
2437
+ ];
2438
+ return {
2439
+ success: false,
2440
+ error: msg
2441
+ };
2442
+ }
2443
+ if (maxPrefix !== void 0 && prefix > maxPrefix) {
2444
+ const errorMsg = errors?.prefixAboveMaximum;
2445
+ if (typeof errorMsg === "function") return {
2446
+ success: false,
2447
+ error: errorMsg(prefix, maxPrefix)
2448
+ };
2449
+ const msg = errorMsg ?? [
2450
+ {
2451
+ type: "text",
2452
+ text: "Expected a prefix length less than or equal to "
2453
+ },
2454
+ {
2455
+ type: "text",
2456
+ text: maxPrefix.toString()
2457
+ },
2458
+ {
2459
+ type: "text",
2460
+ text: ", but got "
2461
+ },
2462
+ {
2463
+ type: "text",
2464
+ text: prefix.toString()
2465
+ },
2466
+ {
2467
+ type: "text",
2468
+ text: "."
2469
+ }
2470
+ ];
2471
+ return {
2472
+ success: false,
2473
+ error: msg
2474
+ };
2475
+ }
2476
+ return {
2477
+ success: true,
2478
+ value: {
2479
+ address: normalizedIp,
2480
+ prefix,
2481
+ version: ipVersion
2482
+ }
2483
+ };
2484
+ },
2485
+ format() {
2486
+ return metavar;
2487
+ }
2488
+ };
2489
+ }
652
2490
 
653
2491
  //#endregion
654
- export { choice, ensureNonEmptyString, float, integer, isNonEmptyString, isValueParser, locale, string, url, uuid };
2492
+ export { choice, cidr, domain, email, ensureNonEmptyString, float, hostname, integer, ip, ipv4, ipv6, isNonEmptyString, isValueParser, locale, macAddress, port, portRange, socketAddress, string, url, uuid };