@optique/core 0.10.0-dev.335 → 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.
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/valueparser.cjs +1849 -0
- package/dist/valueparser.d.cts +1271 -1
- package/dist/valueparser.d.ts +1271 -1
- package/dist/valueparser.js +1839 -1
- package/package.json +1 -1
package/dist/valueparser.js
CHANGED
|
@@ -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 };
|