@optique/core 1.1.0-dev.2146 → 1.1.0-dev.2152

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.
@@ -3088,7 +3088,7 @@ function email(options) {
3088
3088
  * Creates a value parser for socket addresses in "host:port" format.
3089
3089
  *
3090
3090
  * Validates socket addresses with support for:
3091
- * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
3091
+ * - Hostnames, IPv4 addresses, and IPv6 addresses
3092
3092
  * - Configurable host:port separator
3093
3093
  * - Optional default port
3094
3094
  * - Host type filtering (hostname only, IP only, or both)
@@ -3099,6 +3099,8 @@ function email(options) {
3099
3099
  * @throws {TypeError} If `separator` is an empty string.
3100
3100
  * @throws {TypeError} If `separator` contains digit characters, since digits
3101
3101
  * in the separator would cause ambiguous splitting of port input.
3102
+ * @throws {TypeError} If `host.version` is provided but is not `4`, `6`, or
3103
+ * `"both"`.
3102
3104
  * @since 0.10.0
3103
3105
  *
3104
3106
  * @example
@@ -3128,12 +3130,25 @@ function socketAddress(options) {
3128
3130
  const defaultPort = options?.defaultPort;
3129
3131
  const requirePort = options?.requirePort ?? false;
3130
3132
  const hostType = options?.host?.type ?? "both";
3133
+ const rawHostVersion = options?.host?.version;
3134
+ if (rawHostVersion !== void 0 && rawHostVersion !== 4 && rawHostVersion !== 6 && rawHostVersion !== "both") throw new TypeError(`Expected host.version to be 4, 6, or "both", but got ${Array.isArray(rawHostVersion) ? "array" : typeof rawHostVersion}: ${String(rawHostVersion)}.`);
3135
+ const hasLegacyIpOptions = options?.host?.ip !== void 0;
3136
+ const hasNewIpOptions = rawHostVersion !== void 0 || options?.host?.ipv4 !== void 0 || options?.host?.ipv6 !== void 0;
3137
+ const hostVersion = rawHostVersion ?? (hasLegacyIpOptions && !hasNewIpOptions ? 4 : "both");
3131
3138
  const hostnameParser = hostname({
3132
3139
  ...options?.host?.hostname,
3133
3140
  metavar: "HOST"
3134
3141
  });
3135
- const ipParser = ipv4({
3142
+ const ipv4Options = {
3136
3143
  ...options?.host?.ip,
3144
+ ...options?.host?.ipv4
3145
+ };
3146
+ const ipv4Parser = ipv4({
3147
+ ...ipv4Options,
3148
+ metavar: "HOST"
3149
+ });
3150
+ const ipv6Parser = ipv6({
3151
+ ...options?.host?.ipv6,
3137
3152
  metavar: "HOST"
3138
3153
  });
3139
3154
  const disambiguationParser = hostType === "ip" ? hostname({
@@ -3151,6 +3166,15 @@ function socketAddress(options) {
3151
3166
  function looksLikeIpv4(input) {
3152
3167
  return /^\d+\.\d+\.\d+\.\d+$/.test(input);
3153
3168
  }
3169
+ function looksLikeIpv6(input) {
3170
+ const colonCount = input.match(/:/g)?.length ?? 0;
3171
+ return colonCount >= 2 && (input.includes("::") || /^[0-9a-fA-F:.]+$/.test(input));
3172
+ }
3173
+ function looksLikeBareIpv6(input) {
3174
+ if (!looksLikeIpv6(input)) return false;
3175
+ if (parseAndNormalizeIpv6(input) !== null) return true;
3176
+ return input.includes("::") || !input.includes(".");
3177
+ }
3154
3178
  function looksLikeAltIpv4Literal(input) {
3155
3179
  if (/^0[xX][0-9a-fA-F]+$/.test(input)) {
3156
3180
  const n = parseInt(input.slice(2), 16);
@@ -3176,6 +3200,22 @@ function socketAddress(options) {
3176
3200
  }
3177
3201
  return false;
3178
3202
  }
3203
+ function parseIpHost(hostInput) {
3204
+ if (hostVersion === 4) return ipv4Parser.parse(hostInput);
3205
+ if (hostVersion === 6) return ipv6Parser.parse(hostInput);
3206
+ if (looksLikeIpv6(hostInput)) {
3207
+ const result = ipv6Parser.parse(hostInput);
3208
+ if (result.success) {
3209
+ const mappedOctets = extractIpv4FromMapped(result.value);
3210
+ if (mappedOctets !== null) {
3211
+ const restrictionError = checkIpv4MappedRestrictions(mappedOctets, result.value, ipv4Options, void 0);
3212
+ if (restrictionError !== null) return restrictionError;
3213
+ }
3214
+ }
3215
+ return result;
3216
+ }
3217
+ return ipv4Parser.parse(hostInput);
3218
+ }
3179
3219
  function parseHost(hostInput) {
3180
3220
  if (hostType === "hostname") {
3181
3221
  if (looksLikeAltIpv4Literal(hostInput)) return {
@@ -3187,22 +3227,106 @@ function socketAddress(options) {
3187
3227
  error: require_message.message`Expected a valid hostname, but got ${hostInput}.`
3188
3228
  };
3189
3229
  return hostnameParser.parse(hostInput);
3190
- } else if (hostType === "ip") return ipParser.parse(hostInput);
3230
+ } else if (hostType === "ip") return parseIpHost(hostInput);
3191
3231
  else {
3192
3232
  if (looksLikeAltIpv4Literal(hostInput)) return {
3193
3233
  success: false,
3194
3234
  error: require_message.message`${hostInput} appears to be a non-standard IPv4 address notation.`
3195
3235
  };
3196
- if (looksLikeIpv4(hostInput)) return ipParser.parse(hostInput);
3236
+ if (looksLikeIpv4(hostInput)) return parseIpHost(hostInput);
3237
+ if (looksLikeIpv6(hostInput)) return parseIpHost(hostInput);
3197
3238
  return hostnameParser.parse(hostInput);
3198
3239
  }
3199
3240
  }
3200
- return {
3241
+ function makeInvalidFormatError(input) {
3242
+ const errorMsg = options?.errors?.invalidFormat;
3243
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a socket address in format ${require_message.text(formatExample)}, but got ${input}.`;
3244
+ return {
3245
+ success: false,
3246
+ error: msg
3247
+ };
3248
+ }
3249
+ function makeMissingPortError(input) {
3250
+ const errorMsg = options?.errors?.missingPort;
3251
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Port number is required but was not specified.`;
3252
+ return {
3253
+ success: false,
3254
+ error: msg
3255
+ };
3256
+ }
3257
+ function parseBracketedHost(input, trimmed, canOmitPort) {
3258
+ if (!trimmed.startsWith("[")) return void 0;
3259
+ const closingBracket = trimmed.indexOf("]");
3260
+ if (closingBracket === -1) return makeInvalidFormatError(input);
3261
+ const hostInput = trimmed.slice(1, closingBracket);
3262
+ if (!looksLikeBareIpv6(hostInput)) return makeInvalidFormatError(input);
3263
+ const rest = trimmed.slice(closingBracket + 1).trimStart();
3264
+ const hostResult = parseHost(hostInput);
3265
+ if (rest === "") {
3266
+ if (!hostResult.success) {
3267
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3268
+ return {
3269
+ success: false,
3270
+ error: hostResult.error
3271
+ };
3272
+ }
3273
+ if (canOmitPort) return {
3274
+ success: true,
3275
+ value: {
3276
+ host: hostResult.value,
3277
+ port: defaultPort
3278
+ }
3279
+ };
3280
+ return makeMissingPortError(input);
3281
+ }
3282
+ if (!rest.startsWith(separator)) return makeInvalidFormatError(input);
3283
+ const portPart = rest.slice(separator.length).trim();
3284
+ if (portPart === "") {
3285
+ if (!hostResult.success) {
3286
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3287
+ return {
3288
+ success: false,
3289
+ error: hostResult.error
3290
+ };
3291
+ }
3292
+ return makeMissingPortError(input);
3293
+ }
3294
+ const portResult = portParser.parse(portPart);
3295
+ if (!hostResult.success) {
3296
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3297
+ return {
3298
+ success: false,
3299
+ error: hostResult.error
3300
+ };
3301
+ }
3302
+ if (portResult.success) return {
3303
+ success: true,
3304
+ value: {
3305
+ host: hostResult.value,
3306
+ port: portResult.value
3307
+ }
3308
+ };
3309
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3310
+ if (/^[0-9]+$/.test(portPart)) return {
3311
+ success: false,
3312
+ error: portResult.error
3313
+ };
3314
+ return makeInvalidFormatError(input);
3315
+ }
3316
+ function isSocketAddressValue(value) {
3317
+ return typeof value === "object" && value !== null && "host" in value && typeof value.host === "string" && "port" in value && typeof value.port === "number";
3318
+ }
3319
+ function formatSocketAddressValue(value) {
3320
+ const ipv6Host = parseAndNormalizeIpv6(value.host);
3321
+ if (separator === ":" && ipv6Host !== null) return `[${ipv6Host}]${separator}${value.port}`;
3322
+ return `${value.host}${separator}${value.port}`;
3323
+ }
3324
+ const parser = {
3201
3325
  mode: "sync",
3202
3326
  metavar: metavar$1,
3203
3327
  get placeholder() {
3204
3328
  return {
3205
- host: hostType === "ip" ? ipParser.placeholder : hostnameParser.placeholder,
3329
+ host: hostType === "ip" ? hostVersion === 6 ? ipv6Parser.placeholder : ipv4Parser.placeholder : hostnameParser.placeholder,
3206
3330
  port: defaultPort ?? portParser.placeholder
3207
3331
  };
3208
3332
  },
@@ -3210,6 +3334,28 @@ function socketAddress(options) {
3210
3334
  const trimmed = input.trim();
3211
3335
  const searchInput = input.trimStart();
3212
3336
  const canOmitPort = defaultPort !== void 0 && !requirePort;
3337
+ if (separator === ":") {
3338
+ const bracketedResult = parseBracketedHost(input, trimmed, canOmitPort);
3339
+ if (bracketedResult !== void 0) return bracketedResult;
3340
+ if (looksLikeBareIpv6(trimmed)) {
3341
+ const hostResult = parseHost(trimmed);
3342
+ if (hostResult.success) {
3343
+ if (canOmitPort) return {
3344
+ success: true,
3345
+ value: {
3346
+ host: hostResult.value,
3347
+ port: defaultPort
3348
+ }
3349
+ };
3350
+ return makeMissingPortError(input);
3351
+ }
3352
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3353
+ return {
3354
+ success: false,
3355
+ error: hostResult.error
3356
+ };
3357
+ }
3358
+ }
3213
3359
  let firstHostError;
3214
3360
  let validHostInvalidPortError;
3215
3361
  let trailingSepHost;
@@ -3407,9 +3553,19 @@ function socketAddress(options) {
3407
3553
  };
3408
3554
  },
3409
3555
  format(value) {
3410
- return `${value.host}${separator}${value.port}`;
3556
+ return formatSocketAddressValue(value);
3557
+ },
3558
+ normalize(value) {
3559
+ if (!isSocketAddressValue(value)) return value;
3560
+ try {
3561
+ const result = parser.parse(formatSocketAddressValue(value));
3562
+ return result.success ? result.value : value;
3563
+ } catch {
3564
+ return value;
3565
+ }
3411
3566
  }
3412
3567
  };
3568
+ return parser;
3413
3569
  }
3414
3570
  function portRange(options) {
3415
3571
  checkBooleanOption(options, "disallowWellKnown");
@@ -4323,7 +4479,7 @@ function parseAndNormalizeIpv6(input) {
4323
4479
  const leftGroups = parts[0] ? parts[0].split(":") : [];
4324
4480
  const rightGroups = parts[1] ? parts[1].split(":") : [];
4325
4481
  const totalGroups = leftGroups.length + rightGroups.length;
4326
- if (totalGroups > 8) return null;
4482
+ if (totalGroups >= 8) return null;
4327
4483
  const zeroCount = 8 - totalGroups;
4328
4484
  const zeros = Array(zeroCount).fill("0");
4329
4485
  groups = [
@@ -4354,7 +4510,7 @@ function expandIpv6(input) {
4354
4510
  const leftGroups = parts[0] ? parts[0].split(":").filter((g) => g) : [];
4355
4511
  const rightGroups = parts[1] ? parts[1].split(":").filter((g) => g) : [];
4356
4512
  const totalGroups = leftGroups.length + rightGroups.length;
4357
- if (totalGroups > 8) return null;
4513
+ if (totalGroups >= 8) return null;
4358
4514
  const zeroCount = 8 - totalGroups;
4359
4515
  const zeros = Array(zeroCount).fill("0");
4360
4516
  const groups = [
@@ -1572,10 +1572,34 @@ interface SocketAddressOptions {
1572
1572
  */
1573
1573
  readonly hostname?: Omit<HostnameOptions, "metavar" | "errors">;
1574
1574
  /**
1575
- * Options for IP validation (when type is "ip" or "both").
1576
- * Currently only supports IPv4.
1575
+ * IP version to accept when `type` is `"ip"` or `"both"`.
1576
+ * - `4`: IPv4 only
1577
+ * - `6`: IPv6 only
1578
+ * - `"both"`: Accept both IPv4 and IPv6
1579
+ *
1580
+ * @default `"both"` unless only the legacy {@link ip} field is set, in
1581
+ * which case the default is `4` to preserve IPv4-only compatibility.
1582
+ * @since 1.1.0
1583
+ */
1584
+ readonly version?: 4 | 6 | "both";
1585
+ /**
1586
+ * Options for IPv4 validation (when type is "ip" or "both").
1587
+ * This is kept for backwards compatibility; prefer {@link ipv4} for
1588
+ * new code.
1577
1589
  */
1578
1590
  readonly ip?: Omit<Ipv4Options, "metavar" | "errors">;
1591
+ /**
1592
+ * Options for IPv4 validation (when type is "ip" or "both").
1593
+ *
1594
+ * @since 1.1.0
1595
+ */
1596
+ readonly ipv4?: Omit<Ipv4Options, "metavar" | "errors">;
1597
+ /**
1598
+ * Options for IPv6 validation (when type is "ip" or "both").
1599
+ *
1600
+ * @since 1.1.0
1601
+ */
1602
+ readonly ipv6?: Omit<Ipv6Options, "metavar" | "errors">;
1579
1603
  };
1580
1604
  /**
1581
1605
  * Options for port validation.
@@ -1601,7 +1625,7 @@ interface SocketAddressOptions {
1601
1625
  * Creates a value parser for socket addresses in "host:port" format.
1602
1626
  *
1603
1627
  * Validates socket addresses with support for:
1604
- * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
1628
+ * - Hostnames, IPv4 addresses, and IPv6 addresses
1605
1629
  * - Configurable host:port separator
1606
1630
  * - Optional default port
1607
1631
  * - Host type filtering (hostname only, IP only, or both)
@@ -1612,6 +1636,8 @@ interface SocketAddressOptions {
1612
1636
  * @throws {TypeError} If `separator` is an empty string.
1613
1637
  * @throws {TypeError} If `separator` contains digit characters, since digits
1614
1638
  * in the separator would cause ambiguous splitting of port input.
1639
+ * @throws {TypeError} If `host.version` is provided but is not `4`, `6`, or
1640
+ * `"both"`.
1615
1641
  * @since 0.10.0
1616
1642
  *
1617
1643
  * @example
@@ -1572,10 +1572,34 @@ interface SocketAddressOptions {
1572
1572
  */
1573
1573
  readonly hostname?: Omit<HostnameOptions, "metavar" | "errors">;
1574
1574
  /**
1575
- * Options for IP validation (when type is "ip" or "both").
1576
- * Currently only supports IPv4.
1575
+ * IP version to accept when `type` is `"ip"` or `"both"`.
1576
+ * - `4`: IPv4 only
1577
+ * - `6`: IPv6 only
1578
+ * - `"both"`: Accept both IPv4 and IPv6
1579
+ *
1580
+ * @default `"both"` unless only the legacy {@link ip} field is set, in
1581
+ * which case the default is `4` to preserve IPv4-only compatibility.
1582
+ * @since 1.1.0
1583
+ */
1584
+ readonly version?: 4 | 6 | "both";
1585
+ /**
1586
+ * Options for IPv4 validation (when type is "ip" or "both").
1587
+ * This is kept for backwards compatibility; prefer {@link ipv4} for
1588
+ * new code.
1577
1589
  */
1578
1590
  readonly ip?: Omit<Ipv4Options, "metavar" | "errors">;
1591
+ /**
1592
+ * Options for IPv4 validation (when type is "ip" or "both").
1593
+ *
1594
+ * @since 1.1.0
1595
+ */
1596
+ readonly ipv4?: Omit<Ipv4Options, "metavar" | "errors">;
1597
+ /**
1598
+ * Options for IPv6 validation (when type is "ip" or "both").
1599
+ *
1600
+ * @since 1.1.0
1601
+ */
1602
+ readonly ipv6?: Omit<Ipv6Options, "metavar" | "errors">;
1579
1603
  };
1580
1604
  /**
1581
1605
  * Options for port validation.
@@ -1601,7 +1625,7 @@ interface SocketAddressOptions {
1601
1625
  * Creates a value parser for socket addresses in "host:port" format.
1602
1626
  *
1603
1627
  * Validates socket addresses with support for:
1604
- * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
1628
+ * - Hostnames, IPv4 addresses, and IPv6 addresses
1605
1629
  * - Configurable host:port separator
1606
1630
  * - Optional default port
1607
1631
  * - Host type filtering (hostname only, IP only, or both)
@@ -1612,6 +1636,8 @@ interface SocketAddressOptions {
1612
1636
  * @throws {TypeError} If `separator` is an empty string.
1613
1637
  * @throws {TypeError} If `separator` contains digit characters, since digits
1614
1638
  * in the separator would cause ambiguous splitting of port input.
1639
+ * @throws {TypeError} If `host.version` is provided but is not `4`, `6`, or
1640
+ * `"both"`.
1615
1641
  * @since 0.10.0
1616
1642
  *
1617
1643
  * @example
@@ -3088,7 +3088,7 @@ function email(options) {
3088
3088
  * Creates a value parser for socket addresses in "host:port" format.
3089
3089
  *
3090
3090
  * Validates socket addresses with support for:
3091
- * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
3091
+ * - Hostnames, IPv4 addresses, and IPv6 addresses
3092
3092
  * - Configurable host:port separator
3093
3093
  * - Optional default port
3094
3094
  * - Host type filtering (hostname only, IP only, or both)
@@ -3099,6 +3099,8 @@ function email(options) {
3099
3099
  * @throws {TypeError} If `separator` is an empty string.
3100
3100
  * @throws {TypeError} If `separator` contains digit characters, since digits
3101
3101
  * in the separator would cause ambiguous splitting of port input.
3102
+ * @throws {TypeError} If `host.version` is provided but is not `4`, `6`, or
3103
+ * `"both"`.
3102
3104
  * @since 0.10.0
3103
3105
  *
3104
3106
  * @example
@@ -3128,12 +3130,25 @@ function socketAddress(options) {
3128
3130
  const defaultPort = options?.defaultPort;
3129
3131
  const requirePort = options?.requirePort ?? false;
3130
3132
  const hostType = options?.host?.type ?? "both";
3133
+ const rawHostVersion = options?.host?.version;
3134
+ if (rawHostVersion !== void 0 && rawHostVersion !== 4 && rawHostVersion !== 6 && rawHostVersion !== "both") throw new TypeError(`Expected host.version to be 4, 6, or "both", but got ${Array.isArray(rawHostVersion) ? "array" : typeof rawHostVersion}: ${String(rawHostVersion)}.`);
3135
+ const hasLegacyIpOptions = options?.host?.ip !== void 0;
3136
+ const hasNewIpOptions = rawHostVersion !== void 0 || options?.host?.ipv4 !== void 0 || options?.host?.ipv6 !== void 0;
3137
+ const hostVersion = rawHostVersion ?? (hasLegacyIpOptions && !hasNewIpOptions ? 4 : "both");
3131
3138
  const hostnameParser = hostname({
3132
3139
  ...options?.host?.hostname,
3133
3140
  metavar: "HOST"
3134
3141
  });
3135
- const ipParser = ipv4({
3142
+ const ipv4Options = {
3136
3143
  ...options?.host?.ip,
3144
+ ...options?.host?.ipv4
3145
+ };
3146
+ const ipv4Parser = ipv4({
3147
+ ...ipv4Options,
3148
+ metavar: "HOST"
3149
+ });
3150
+ const ipv6Parser = ipv6({
3151
+ ...options?.host?.ipv6,
3137
3152
  metavar: "HOST"
3138
3153
  });
3139
3154
  const disambiguationParser = hostType === "ip" ? hostname({
@@ -3151,6 +3166,15 @@ function socketAddress(options) {
3151
3166
  function looksLikeIpv4(input) {
3152
3167
  return /^\d+\.\d+\.\d+\.\d+$/.test(input);
3153
3168
  }
3169
+ function looksLikeIpv6(input) {
3170
+ const colonCount = input.match(/:/g)?.length ?? 0;
3171
+ return colonCount >= 2 && (input.includes("::") || /^[0-9a-fA-F:.]+$/.test(input));
3172
+ }
3173
+ function looksLikeBareIpv6(input) {
3174
+ if (!looksLikeIpv6(input)) return false;
3175
+ if (parseAndNormalizeIpv6(input) !== null) return true;
3176
+ return input.includes("::") || !input.includes(".");
3177
+ }
3154
3178
  function looksLikeAltIpv4Literal(input) {
3155
3179
  if (/^0[xX][0-9a-fA-F]+$/.test(input)) {
3156
3180
  const n = parseInt(input.slice(2), 16);
@@ -3176,6 +3200,22 @@ function socketAddress(options) {
3176
3200
  }
3177
3201
  return false;
3178
3202
  }
3203
+ function parseIpHost(hostInput) {
3204
+ if (hostVersion === 4) return ipv4Parser.parse(hostInput);
3205
+ if (hostVersion === 6) return ipv6Parser.parse(hostInput);
3206
+ if (looksLikeIpv6(hostInput)) {
3207
+ const result = ipv6Parser.parse(hostInput);
3208
+ if (result.success) {
3209
+ const mappedOctets = extractIpv4FromMapped(result.value);
3210
+ if (mappedOctets !== null) {
3211
+ const restrictionError = checkIpv4MappedRestrictions(mappedOctets, result.value, ipv4Options, void 0);
3212
+ if (restrictionError !== null) return restrictionError;
3213
+ }
3214
+ }
3215
+ return result;
3216
+ }
3217
+ return ipv4Parser.parse(hostInput);
3218
+ }
3179
3219
  function parseHost(hostInput) {
3180
3220
  if (hostType === "hostname") {
3181
3221
  if (looksLikeAltIpv4Literal(hostInput)) return {
@@ -3187,22 +3227,106 @@ function socketAddress(options) {
3187
3227
  error: message`Expected a valid hostname, but got ${hostInput}.`
3188
3228
  };
3189
3229
  return hostnameParser.parse(hostInput);
3190
- } else if (hostType === "ip") return ipParser.parse(hostInput);
3230
+ } else if (hostType === "ip") return parseIpHost(hostInput);
3191
3231
  else {
3192
3232
  if (looksLikeAltIpv4Literal(hostInput)) return {
3193
3233
  success: false,
3194
3234
  error: message`${hostInput} appears to be a non-standard IPv4 address notation.`
3195
3235
  };
3196
- if (looksLikeIpv4(hostInput)) return ipParser.parse(hostInput);
3236
+ if (looksLikeIpv4(hostInput)) return parseIpHost(hostInput);
3237
+ if (looksLikeIpv6(hostInput)) return parseIpHost(hostInput);
3197
3238
  return hostnameParser.parse(hostInput);
3198
3239
  }
3199
3240
  }
3200
- return {
3241
+ function makeInvalidFormatError(input) {
3242
+ const errorMsg = options?.errors?.invalidFormat;
3243
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a socket address in format ${text(formatExample)}, but got ${input}.`;
3244
+ return {
3245
+ success: false,
3246
+ error: msg
3247
+ };
3248
+ }
3249
+ function makeMissingPortError(input) {
3250
+ const errorMsg = options?.errors?.missingPort;
3251
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Port number is required but was not specified.`;
3252
+ return {
3253
+ success: false,
3254
+ error: msg
3255
+ };
3256
+ }
3257
+ function parseBracketedHost(input, trimmed, canOmitPort) {
3258
+ if (!trimmed.startsWith("[")) return void 0;
3259
+ const closingBracket = trimmed.indexOf("]");
3260
+ if (closingBracket === -1) return makeInvalidFormatError(input);
3261
+ const hostInput = trimmed.slice(1, closingBracket);
3262
+ if (!looksLikeBareIpv6(hostInput)) return makeInvalidFormatError(input);
3263
+ const rest = trimmed.slice(closingBracket + 1).trimStart();
3264
+ const hostResult = parseHost(hostInput);
3265
+ if (rest === "") {
3266
+ if (!hostResult.success) {
3267
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3268
+ return {
3269
+ success: false,
3270
+ error: hostResult.error
3271
+ };
3272
+ }
3273
+ if (canOmitPort) return {
3274
+ success: true,
3275
+ value: {
3276
+ host: hostResult.value,
3277
+ port: defaultPort
3278
+ }
3279
+ };
3280
+ return makeMissingPortError(input);
3281
+ }
3282
+ if (!rest.startsWith(separator)) return makeInvalidFormatError(input);
3283
+ const portPart = rest.slice(separator.length).trim();
3284
+ if (portPart === "") {
3285
+ if (!hostResult.success) {
3286
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3287
+ return {
3288
+ success: false,
3289
+ error: hostResult.error
3290
+ };
3291
+ }
3292
+ return makeMissingPortError(input);
3293
+ }
3294
+ const portResult = portParser.parse(portPart);
3295
+ if (!hostResult.success) {
3296
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3297
+ return {
3298
+ success: false,
3299
+ error: hostResult.error
3300
+ };
3301
+ }
3302
+ if (portResult.success) return {
3303
+ success: true,
3304
+ value: {
3305
+ host: hostResult.value,
3306
+ port: portResult.value
3307
+ }
3308
+ };
3309
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3310
+ if (/^[0-9]+$/.test(portPart)) return {
3311
+ success: false,
3312
+ error: portResult.error
3313
+ };
3314
+ return makeInvalidFormatError(input);
3315
+ }
3316
+ function isSocketAddressValue(value) {
3317
+ return typeof value === "object" && value !== null && "host" in value && typeof value.host === "string" && "port" in value && typeof value.port === "number";
3318
+ }
3319
+ function formatSocketAddressValue(value) {
3320
+ const ipv6Host = parseAndNormalizeIpv6(value.host);
3321
+ if (separator === ":" && ipv6Host !== null) return `[${ipv6Host}]${separator}${value.port}`;
3322
+ return `${value.host}${separator}${value.port}`;
3323
+ }
3324
+ const parser = {
3201
3325
  mode: "sync",
3202
3326
  metavar: metavar$1,
3203
3327
  get placeholder() {
3204
3328
  return {
3205
- host: hostType === "ip" ? ipParser.placeholder : hostnameParser.placeholder,
3329
+ host: hostType === "ip" ? hostVersion === 6 ? ipv6Parser.placeholder : ipv4Parser.placeholder : hostnameParser.placeholder,
3206
3330
  port: defaultPort ?? portParser.placeholder
3207
3331
  };
3208
3332
  },
@@ -3210,6 +3334,28 @@ function socketAddress(options) {
3210
3334
  const trimmed = input.trim();
3211
3335
  const searchInput = input.trimStart();
3212
3336
  const canOmitPort = defaultPort !== void 0 && !requirePort;
3337
+ if (separator === ":") {
3338
+ const bracketedResult = parseBracketedHost(input, trimmed, canOmitPort);
3339
+ if (bracketedResult !== void 0) return bracketedResult;
3340
+ if (looksLikeBareIpv6(trimmed)) {
3341
+ const hostResult = parseHost(trimmed);
3342
+ if (hostResult.success) {
3343
+ if (canOmitPort) return {
3344
+ success: true,
3345
+ value: {
3346
+ host: hostResult.value,
3347
+ port: defaultPort
3348
+ }
3349
+ };
3350
+ return makeMissingPortError(input);
3351
+ }
3352
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3353
+ return {
3354
+ success: false,
3355
+ error: hostResult.error
3356
+ };
3357
+ }
3358
+ }
3213
3359
  let firstHostError;
3214
3360
  let validHostInvalidPortError;
3215
3361
  let trailingSepHost;
@@ -3407,9 +3553,19 @@ function socketAddress(options) {
3407
3553
  };
3408
3554
  },
3409
3555
  format(value) {
3410
- return `${value.host}${separator}${value.port}`;
3556
+ return formatSocketAddressValue(value);
3557
+ },
3558
+ normalize(value) {
3559
+ if (!isSocketAddressValue(value)) return value;
3560
+ try {
3561
+ const result = parser.parse(formatSocketAddressValue(value));
3562
+ return result.success ? result.value : value;
3563
+ } catch {
3564
+ return value;
3565
+ }
3411
3566
  }
3412
3567
  };
3568
+ return parser;
3413
3569
  }
3414
3570
  function portRange(options) {
3415
3571
  checkBooleanOption(options, "disallowWellKnown");
@@ -4323,7 +4479,7 @@ function parseAndNormalizeIpv6(input) {
4323
4479
  const leftGroups = parts[0] ? parts[0].split(":") : [];
4324
4480
  const rightGroups = parts[1] ? parts[1].split(":") : [];
4325
4481
  const totalGroups = leftGroups.length + rightGroups.length;
4326
- if (totalGroups > 8) return null;
4482
+ if (totalGroups >= 8) return null;
4327
4483
  const zeroCount = 8 - totalGroups;
4328
4484
  const zeros = Array(zeroCount).fill("0");
4329
4485
  groups = [
@@ -4354,7 +4510,7 @@ function expandIpv6(input) {
4354
4510
  const leftGroups = parts[0] ? parts[0].split(":").filter((g) => g) : [];
4355
4511
  const rightGroups = parts[1] ? parts[1].split(":").filter((g) => g) : [];
4356
4512
  const totalGroups = leftGroups.length + rightGroups.length;
4357
- if (totalGroups > 8) return null;
4513
+ if (totalGroups >= 8) return null;
4358
4514
  const zeroCount = 8 - totalGroups;
4359
4515
  const zeros = Array(zeroCount).fill("0");
4360
4516
  const groups = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.1.0-dev.2146",
3
+ "version": "1.1.0-dev.2152",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -200,7 +200,7 @@
200
200
  "fast-check": "^4.7.0",
201
201
  "tsdown": "^0.13.0",
202
202
  "typescript": "^5.8.3",
203
- "@optique/env": "1.1.0-dev.2146+5fd0a75e"
203
+ "@optique/env": "1.1.0-dev.2152+cc0a0741"
204
204
  },
205
205
  "scripts": {
206
206
  "build": "tsdown",