@optique/core 1.0.0-dev.1561 → 1.0.0-dev.1572

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.
@@ -941,18 +941,20 @@ function isPlainObject(value) {
941
941
  function collectDependencyValues(deferredState, registry) {
942
942
  const depIds = deferredState.dependencyIds;
943
943
  if (depIds && depIds.length > 0) {
944
- const defaults = deferredState.defaultValues;
944
+ const defaults$1 = deferredState.defaultValues;
945
945
  const dependencyValues = [];
946
946
  for (let i = 0; i < depIds.length; i++) {
947
947
  const depId$1 = depIds[i];
948
948
  if (registry.has(depId$1)) dependencyValues.push(registry.get(depId$1));
949
- else if (defaults && i < defaults.length) dependencyValues.push(defaults[i]);
949
+ else if (defaults$1 && i < defaults$1.length) dependencyValues.push(defaults$1[i]);
950
950
  else return null;
951
951
  }
952
952
  return dependencyValues;
953
953
  }
954
954
  const depId = deferredState.dependencyId;
955
955
  if (registry.has(depId)) return registry.get(depId);
956
+ const defaults = deferredState.defaultValues;
957
+ if (defaults && defaults.length > 0) return defaults[0];
956
958
  return null;
957
959
  }
958
960
  /**
@@ -941,18 +941,20 @@ function isPlainObject(value) {
941
941
  function collectDependencyValues(deferredState, registry) {
942
942
  const depIds = deferredState.dependencyIds;
943
943
  if (depIds && depIds.length > 0) {
944
- const defaults = deferredState.defaultValues;
944
+ const defaults$1 = deferredState.defaultValues;
945
945
  const dependencyValues = [];
946
946
  for (let i = 0; i < depIds.length; i++) {
947
947
  const depId$1 = depIds[i];
948
948
  if (registry.has(depId$1)) dependencyValues.push(registry.get(depId$1));
949
- else if (defaults && i < defaults.length) dependencyValues.push(defaults[i]);
949
+ else if (defaults$1 && i < defaults$1.length) dependencyValues.push(defaults$1[i]);
950
950
  else return null;
951
951
  }
952
952
  return dependencyValues;
953
953
  }
954
954
  const depId = deferredState.dependencyId;
955
955
  if (registry.has(depId)) return registry.get(depId);
956
+ const defaults = deferredState.defaultValues;
957
+ if (defaults && defaults.length > 0) return defaults[0];
956
958
  return null;
957
959
  }
958
960
  /**
@@ -96,7 +96,9 @@ interface SourceContext<TRequiredOptions = void> {
96
96
  * Unique identifier for this context.
97
97
  *
98
98
  * This symbol is typically the same as the annotation key used by parsers
99
- * that consume this context's data.
99
+ * that consume this context's data. Passing multiple contexts with the
100
+ * same id to {@link runWith}, {@link runWithSync}, or {@link runWithAsync}
101
+ * throws a `TypeError`.
100
102
  */
101
103
  readonly id: symbol;
102
104
  /**
package/dist/context.d.ts CHANGED
@@ -96,7 +96,9 @@ interface SourceContext<TRequiredOptions = void> {
96
96
  * Unique identifier for this context.
97
97
  *
98
98
  * This symbol is typically the same as the annotation key used by parsers
99
- * that consume this context's data.
99
+ * that consume this context's data. Passing multiple contexts with the
100
+ * same id to {@link runWith}, {@link runWithSync}, or {@link runWithAsync}
101
+ * throws a `TypeError`.
100
102
  */
101
103
  readonly id: symbol;
102
104
  /**
package/dist/facade.cjs CHANGED
@@ -1192,6 +1192,8 @@ function disposeContextsSync(contexts) {
1192
1192
  * @param contexts Source contexts to use (priority: earlier overrides later).
1193
1193
  * @param options Run options including args, help, version, etc.
1194
1194
  * @returns Promise that resolves to the parsed result.
1195
+ * @throws {TypeError} If two or more contexts share the same
1196
+ * {@link SourceContext.id}.
1195
1197
  * @since 0.10.0
1196
1198
  *
1197
1199
  * @example
@@ -1221,6 +1223,7 @@ async function runWith(parser, programName, contexts, options) {
1221
1223
  return Promise.resolve(runParser(parser, programName, args, options));
1222
1224
  }
1223
1225
  try {
1226
+ require_validate.validateContextIds(contexts);
1224
1227
  if (needsEarlyExit(args, options)) {
1225
1228
  if (parser.$mode === "async") return runParser(parser, programName, args, options);
1226
1229
  return Promise.resolve(runParser(parser, programName, args, options));
@@ -1281,6 +1284,8 @@ async function runWith(parser, programName, contexts, options) {
1281
1284
  * @returns The parsed result.
1282
1285
  * @throws {TypeError} If an async parser is passed at runtime. Use
1283
1286
  * {@link runWith} or {@link runWithAsync} for async parsers.
1287
+ * @throws {TypeError} If two or more contexts share the same
1288
+ * {@link SourceContext.id}.
1284
1289
  * @throws {Error} If any context returns a Promise or if a context's
1285
1290
  * `[Symbol.asyncDispose]` returns a Promise.
1286
1291
  * @since 0.10.0
@@ -1290,6 +1295,7 @@ function runWithSync(parser, programName, contexts, options) {
1290
1295
  const args = options?.args ?? [];
1291
1296
  if (contexts.length === 0) return runParser(parser, programName, args, options);
1292
1297
  try {
1298
+ require_validate.validateContextIds(contexts);
1293
1299
  if (needsEarlyExit(args, options)) return runParser(parser, programName, args, options);
1294
1300
  const ctxOptions = options?.contextOptions;
1295
1301
  const { annotations: phase1Annotations, annotationsList: phase1AnnotationsList, hasDynamic: needsTwoPhase } = collectPhase1AnnotationsSync(contexts, ctxOptions);
@@ -1333,6 +1339,8 @@ function runWithSync(parser, programName, contexts, options) {
1333
1339
  * @param contexts Source contexts to use (priority: earlier overrides later).
1334
1340
  * @param options Run options including args, help, version, etc.
1335
1341
  * @returns Promise that resolves to the parsed result.
1342
+ * @throws {TypeError} If two or more contexts share the same
1343
+ * {@link SourceContext.id}.
1336
1344
  * @since 0.10.0
1337
1345
  */
1338
1346
  function runWithAsync(parser, programName, contexts, options) {
package/dist/facade.d.cts CHANGED
@@ -428,6 +428,8 @@ type ContextOptionsParam<TContexts extends readonly SourceContext<unknown>[], TV
428
428
  * @param contexts Source contexts to use (priority: earlier overrides later).
429
429
  * @param options Run options including args, help, version, etc.
430
430
  * @returns Promise that resolves to the parsed result.
431
+ * @throws {TypeError} If two or more contexts share the same
432
+ * {@link SourceContext.id}.
431
433
  * @since 0.10.0
432
434
  *
433
435
  * @example
@@ -467,6 +469,8 @@ declare function runWith<TParser extends Parser<Mode, unknown, unknown>, TContex
467
469
  * @returns The parsed result.
468
470
  * @throws {TypeError} If an async parser is passed at runtime. Use
469
471
  * {@link runWith} or {@link runWithAsync} for async parsers.
472
+ * @throws {TypeError} If two or more contexts share the same
473
+ * {@link SourceContext.id}.
470
474
  * @throws {Error} If any context returns a Promise or if a context's
471
475
  * `[Symbol.asyncDispose]` returns a Promise.
472
476
  * @since 0.10.0
@@ -486,6 +490,8 @@ declare function runWithSync<TParser extends Parser<"sync", unknown, unknown>, T
486
490
  * @param contexts Source contexts to use (priority: earlier overrides later).
487
491
  * @param options Run options including args, help, version, etc.
488
492
  * @returns Promise that resolves to the parsed result.
493
+ * @throws {TypeError} If two or more contexts share the same
494
+ * {@link SourceContext.id}.
489
495
  * @since 0.10.0
490
496
  */
491
497
  declare function runWithAsync<TParser extends Parser<Mode, unknown, unknown>, TContexts extends readonly SourceContext<unknown>[], THelp = void, TError = never>(parser: TParser, programName: string, contexts: TContexts, options: RunWithOptions<THelp, TError> & ContextOptionsParam<TContexts, InferValue<TParser>>): Promise<InferValue<TParser>>;
package/dist/facade.d.ts CHANGED
@@ -428,6 +428,8 @@ type ContextOptionsParam<TContexts extends readonly SourceContext<unknown>[], TV
428
428
  * @param contexts Source contexts to use (priority: earlier overrides later).
429
429
  * @param options Run options including args, help, version, etc.
430
430
  * @returns Promise that resolves to the parsed result.
431
+ * @throws {TypeError} If two or more contexts share the same
432
+ * {@link SourceContext.id}.
431
433
  * @since 0.10.0
432
434
  *
433
435
  * @example
@@ -467,6 +469,8 @@ declare function runWith<TParser extends Parser<Mode, unknown, unknown>, TContex
467
469
  * @returns The parsed result.
468
470
  * @throws {TypeError} If an async parser is passed at runtime. Use
469
471
  * {@link runWith} or {@link runWithAsync} for async parsers.
472
+ * @throws {TypeError} If two or more contexts share the same
473
+ * {@link SourceContext.id}.
470
474
  * @throws {Error} If any context returns a Promise or if a context's
471
475
  * `[Symbol.asyncDispose]` returns a Promise.
472
476
  * @since 0.10.0
@@ -486,6 +490,8 @@ declare function runWithSync<TParser extends Parser<"sync", unknown, unknown>, T
486
490
  * @param contexts Source contexts to use (priority: earlier overrides later).
487
491
  * @param options Run options including args, help, version, etc.
488
492
  * @returns Promise that resolves to the parsed result.
493
+ * @throws {TypeError} If two or more contexts share the same
494
+ * {@link SourceContext.id}.
489
495
  * @since 0.10.0
490
496
  */
491
497
  declare function runWithAsync<TParser extends Parser<Mode, unknown, unknown>, TContexts extends readonly SourceContext<unknown>[], THelp = void, TError = never>(parser: TParser, programName: string, contexts: TContexts, options: RunWithOptions<THelp, TError> & ContextOptionsParam<TContexts, InferValue<TParser>>): Promise<InferValue<TParser>>;
package/dist/facade.js CHANGED
@@ -2,7 +2,7 @@ import { injectAnnotations } from "./annotations.js";
2
2
  import { commandLine, formatMessage, lineBreak, message, optionName, text, value } from "./message.js";
3
3
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
4
4
  import { dispatchByMode } from "./mode-dispatch.js";
5
- import { validateCommandNames, validateMetaNameCollisions, validateOptionNames, validateProgramName } from "./validate.js";
5
+ import { validateCommandNames, validateContextIds, validateMetaNameCollisions, validateOptionNames, validateProgramName } from "./validate.js";
6
6
  import { extractCommandNames, extractLiteralValues, extractOptionNames, formatUsage } from "./usage.js";
7
7
  import { formatDocPage } from "./doc.js";
8
8
  import { group, longestMatch, object } from "./constructs.js";
@@ -1192,6 +1192,8 @@ function disposeContextsSync(contexts) {
1192
1192
  * @param contexts Source contexts to use (priority: earlier overrides later).
1193
1193
  * @param options Run options including args, help, version, etc.
1194
1194
  * @returns Promise that resolves to the parsed result.
1195
+ * @throws {TypeError} If two or more contexts share the same
1196
+ * {@link SourceContext.id}.
1195
1197
  * @since 0.10.0
1196
1198
  *
1197
1199
  * @example
@@ -1221,6 +1223,7 @@ async function runWith(parser, programName, contexts, options) {
1221
1223
  return Promise.resolve(runParser(parser, programName, args, options));
1222
1224
  }
1223
1225
  try {
1226
+ validateContextIds(contexts);
1224
1227
  if (needsEarlyExit(args, options)) {
1225
1228
  if (parser.$mode === "async") return runParser(parser, programName, args, options);
1226
1229
  return Promise.resolve(runParser(parser, programName, args, options));
@@ -1281,6 +1284,8 @@ async function runWith(parser, programName, contexts, options) {
1281
1284
  * @returns The parsed result.
1282
1285
  * @throws {TypeError} If an async parser is passed at runtime. Use
1283
1286
  * {@link runWith} or {@link runWithAsync} for async parsers.
1287
+ * @throws {TypeError} If two or more contexts share the same
1288
+ * {@link SourceContext.id}.
1284
1289
  * @throws {Error} If any context returns a Promise or if a context's
1285
1290
  * `[Symbol.asyncDispose]` returns a Promise.
1286
1291
  * @since 0.10.0
@@ -1290,6 +1295,7 @@ function runWithSync(parser, programName, contexts, options) {
1290
1295
  const args = options?.args ?? [];
1291
1296
  if (contexts.length === 0) return runParser(parser, programName, args, options);
1292
1297
  try {
1298
+ validateContextIds(contexts);
1293
1299
  if (needsEarlyExit(args, options)) return runParser(parser, programName, args, options);
1294
1300
  const ctxOptions = options?.contextOptions;
1295
1301
  const { annotations: phase1Annotations, annotationsList: phase1AnnotationsList, hasDynamic: needsTwoPhase } = collectPhase1AnnotationsSync(contexts, ctxOptions);
@@ -1333,6 +1339,8 @@ function runWithSync(parser, programName, contexts, options) {
1333
1339
  * @param contexts Source contexts to use (priority: earlier overrides later).
1334
1340
  * @param options Run options including args, help, version, etc.
1335
1341
  * @returns Promise that resolves to the parsed result.
1342
+ * @throws {TypeError} If two or more contexts share the same
1343
+ * {@link SourceContext.id}.
1336
1344
  * @since 0.10.0
1337
1345
  */
1338
1346
  function runWithAsync(parser, programName, contexts, options) {
package/dist/validate.cjs CHANGED
@@ -177,9 +177,25 @@ function validateLabel(label) {
177
177
  if (/^\s+$/.test(label)) throw new TypeError(`Label must not be whitespace-only: "${escapeControlChars(label)}".`);
178
178
  if (CONTROL_CHAR_RE.test(label)) throw new TypeError(`Label must not contain control characters: "${escapeControlChars(label)}".`);
179
179
  }
180
+ /**
181
+ * Validates that all source contexts have unique
182
+ * {@link import("./context.ts").SourceContext.id | id} values.
183
+ *
184
+ * @param contexts The source contexts to validate.
185
+ * @throws {TypeError} If two or more contexts share the same id.
186
+ * @since 1.0.0
187
+ */
188
+ function validateContextIds(contexts) {
189
+ const seen = /* @__PURE__ */ new Set();
190
+ for (const context of contexts) {
191
+ if (seen.has(context.id)) throw new TypeError(`Duplicate SourceContext id: ${String(context.id)}`);
192
+ seen.add(context.id);
193
+ }
194
+ }
180
195
 
181
196
  //#endregion
182
197
  exports.validateCommandNames = validateCommandNames;
198
+ exports.validateContextIds = validateContextIds;
183
199
  exports.validateLabel = validateLabel;
184
200
  exports.validateMetaNameCollisions = validateMetaNameCollisions;
185
201
  exports.validateOptionNames = validateOptionNames;
package/dist/validate.js CHANGED
@@ -176,6 +176,21 @@ function validateLabel(label) {
176
176
  if (/^\s+$/.test(label)) throw new TypeError(`Label must not be whitespace-only: "${escapeControlChars(label)}".`);
177
177
  if (CONTROL_CHAR_RE.test(label)) throw new TypeError(`Label must not contain control characters: "${escapeControlChars(label)}".`);
178
178
  }
179
+ /**
180
+ * Validates that all source contexts have unique
181
+ * {@link import("./context.ts").SourceContext.id | id} values.
182
+ *
183
+ * @param contexts The source contexts to validate.
184
+ * @throws {TypeError} If two or more contexts share the same id.
185
+ * @since 1.0.0
186
+ */
187
+ function validateContextIds(contexts) {
188
+ const seen = /* @__PURE__ */ new Set();
189
+ for (const context of contexts) {
190
+ if (seen.has(context.id)) throw new TypeError(`Duplicate SourceContext id: ${String(context.id)}`);
191
+ seen.add(context.id);
192
+ }
193
+ }
179
194
 
180
195
  //#endregion
181
- export { validateCommandNames, validateLabel, validateMetaNameCollisions, validateOptionNames, validateProgramName };
196
+ export { validateCommandNames, validateContextIds, validateLabel, validateMetaNameCollisions, validateOptionNames, validateProgramName };
@@ -1161,8 +1161,8 @@ function ipv4(options) {
1161
1161
  metavar,
1162
1162
  placeholder: allowZero ? "0.0.0.0" : allowLoopback ? "127.0.0.1" : "192.0.2.1",
1163
1163
  parse(input) {
1164
- const parts = input.split(".");
1165
- if (parts.length !== 4) {
1164
+ const octets = parseIpv4Octets(input);
1165
+ if (octets === null) {
1166
1166
  const errorMsg = options?.errors?.invalidIpv4;
1167
1167
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1168
1168
  return {
@@ -1170,51 +1170,6 @@ function ipv4(options) {
1170
1170
  error: msg
1171
1171
  };
1172
1172
  }
1173
- const octets = [];
1174
- for (const part of parts) {
1175
- if (part.length === 0) {
1176
- const errorMsg = options?.errors?.invalidIpv4;
1177
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1178
- return {
1179
- success: false,
1180
- error: msg
1181
- };
1182
- }
1183
- if (part.trim() !== part) {
1184
- const errorMsg = options?.errors?.invalidIpv4;
1185
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1186
- return {
1187
- success: false,
1188
- error: msg
1189
- };
1190
- }
1191
- if (part.length > 1 && part[0] === "0") {
1192
- const errorMsg = options?.errors?.invalidIpv4;
1193
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1194
- return {
1195
- success: false,
1196
- error: msg
1197
- };
1198
- }
1199
- if (!/^[0-9]+$/.test(part)) {
1200
- const errorMsg = options?.errors?.invalidIpv4;
1201
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1202
- return {
1203
- success: false,
1204
- error: msg
1205
- };
1206
- }
1207
- const octet = Number(part);
1208
- if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
1209
- const errorMsg = options?.errors?.invalidIpv4;
1210
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1211
- return {
1212
- success: false,
1213
- error: msg
1214
- };
1215
- }
1216
- octets.push(octet);
1217
- }
1218
1173
  const ipAddress = octets.join(".");
1219
1174
  if (!allowPrivate && isPrivateIp(octets)) {
1220
1175
  const errorMsg = options?.errors?.privateNotAllowed;
@@ -2855,6 +2810,33 @@ function ipv6(options) {
2855
2810
  return ipv6ParserObj;
2856
2811
  }
2857
2812
  /**
2813
+ * Parses a dotted-decimal IPv4 string into four validated octets.
2814
+ * Returns null if the input is not a valid strict IPv4 address
2815
+ * (exactly four decimal octets 0–255, no leading zeros, no whitespace,
2816
+ * no non-decimal characters).
2817
+ */
2818
+ function parseIpv4Octets(input) {
2819
+ const parts = input.split(".");
2820
+ if (parts.length !== 4) return null;
2821
+ const octets = [
2822
+ 0,
2823
+ 0,
2824
+ 0,
2825
+ 0
2826
+ ];
2827
+ for (let i = 0; i < 4; i++) {
2828
+ const part = parts[i];
2829
+ if (part.length === 0) return null;
2830
+ if (part.trim() !== part) return null;
2831
+ if (part.length > 1 && part[0] === "0") return null;
2832
+ if (!/^[0-9]+$/.test(part)) return null;
2833
+ const octet = Number(part);
2834
+ if (!Number.isInteger(octet) || octet < 0 || octet > 255) return null;
2835
+ octets[i] = octet;
2836
+ }
2837
+ return octets;
2838
+ }
2839
+ /**
2858
2840
  * Parses and normalizes an IPv6 address to canonical form.
2859
2841
  * Returns null if the input is not a valid IPv6 address.
2860
2842
  */
@@ -2864,10 +2846,8 @@ function parseAndNormalizeIpv6(input) {
2864
2846
  if (ipv4MappedMatch) {
2865
2847
  const ipv6Part = ipv4MappedMatch[1];
2866
2848
  const ipv4Part = ipv4MappedMatch[2];
2867
- const ipv4Octets = ipv4Part.split(".");
2868
- if (ipv4Octets.length !== 4) return null;
2869
- const octets = ipv4Octets.map((o) => parseInt(o, 10));
2870
- if (octets.some((o) => isNaN(o) || o < 0 || o > 255)) return null;
2849
+ const octets = parseIpv4Octets(ipv4Part);
2850
+ if (octets === null) return null;
2871
2851
  const group1 = octets[0] << 8 | octets[1];
2872
2852
  const group2 = octets[2] << 8 | octets[3];
2873
2853
  const fullAddress = `${ipv6Part}:${group1.toString(16)}:${group2.toString(16)}`;
@@ -1161,8 +1161,8 @@ function ipv4(options) {
1161
1161
  metavar,
1162
1162
  placeholder: allowZero ? "0.0.0.0" : allowLoopback ? "127.0.0.1" : "192.0.2.1",
1163
1163
  parse(input) {
1164
- const parts = input.split(".");
1165
- if (parts.length !== 4) {
1164
+ const octets = parseIpv4Octets(input);
1165
+ if (octets === null) {
1166
1166
  const errorMsg = options?.errors?.invalidIpv4;
1167
1167
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1168
1168
  return {
@@ -1170,51 +1170,6 @@ function ipv4(options) {
1170
1170
  error: msg
1171
1171
  };
1172
1172
  }
1173
- const octets = [];
1174
- for (const part of parts) {
1175
- if (part.length === 0) {
1176
- const errorMsg = options?.errors?.invalidIpv4;
1177
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1178
- return {
1179
- success: false,
1180
- error: msg
1181
- };
1182
- }
1183
- if (part.trim() !== part) {
1184
- const errorMsg = options?.errors?.invalidIpv4;
1185
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1186
- return {
1187
- success: false,
1188
- error: msg
1189
- };
1190
- }
1191
- if (part.length > 1 && part[0] === "0") {
1192
- const errorMsg = options?.errors?.invalidIpv4;
1193
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1194
- return {
1195
- success: false,
1196
- error: msg
1197
- };
1198
- }
1199
- if (!/^[0-9]+$/.test(part)) {
1200
- const errorMsg = options?.errors?.invalidIpv4;
1201
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1202
- return {
1203
- success: false,
1204
- error: msg
1205
- };
1206
- }
1207
- const octet = Number(part);
1208
- if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
1209
- const errorMsg = options?.errors?.invalidIpv4;
1210
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid IPv4 address, but got ${input}.`;
1211
- return {
1212
- success: false,
1213
- error: msg
1214
- };
1215
- }
1216
- octets.push(octet);
1217
- }
1218
1173
  const ipAddress = octets.join(".");
1219
1174
  if (!allowPrivate && isPrivateIp(octets)) {
1220
1175
  const errorMsg = options?.errors?.privateNotAllowed;
@@ -2855,6 +2810,33 @@ function ipv6(options) {
2855
2810
  return ipv6ParserObj;
2856
2811
  }
2857
2812
  /**
2813
+ * Parses a dotted-decimal IPv4 string into four validated octets.
2814
+ * Returns null if the input is not a valid strict IPv4 address
2815
+ * (exactly four decimal octets 0–255, no leading zeros, no whitespace,
2816
+ * no non-decimal characters).
2817
+ */
2818
+ function parseIpv4Octets(input) {
2819
+ const parts = input.split(".");
2820
+ if (parts.length !== 4) return null;
2821
+ const octets = [
2822
+ 0,
2823
+ 0,
2824
+ 0,
2825
+ 0
2826
+ ];
2827
+ for (let i = 0; i < 4; i++) {
2828
+ const part = parts[i];
2829
+ if (part.length === 0) return null;
2830
+ if (part.trim() !== part) return null;
2831
+ if (part.length > 1 && part[0] === "0") return null;
2832
+ if (!/^[0-9]+$/.test(part)) return null;
2833
+ const octet = Number(part);
2834
+ if (!Number.isInteger(octet) || octet < 0 || octet > 255) return null;
2835
+ octets[i] = octet;
2836
+ }
2837
+ return octets;
2838
+ }
2839
+ /**
2858
2840
  * Parses and normalizes an IPv6 address to canonical form.
2859
2841
  * Returns null if the input is not a valid IPv6 address.
2860
2842
  */
@@ -2864,10 +2846,8 @@ function parseAndNormalizeIpv6(input) {
2864
2846
  if (ipv4MappedMatch) {
2865
2847
  const ipv6Part = ipv4MappedMatch[1];
2866
2848
  const ipv4Part = ipv4MappedMatch[2];
2867
- const ipv4Octets = ipv4Part.split(".");
2868
- if (ipv4Octets.length !== 4) return null;
2869
- const octets = ipv4Octets.map((o) => parseInt(o, 10));
2870
- if (octets.some((o) => isNaN(o) || o < 0 || o > 255)) return null;
2849
+ const octets = parseIpv4Octets(ipv4Part);
2850
+ if (octets === null) return null;
2871
2851
  const group1 = octets[0] << 8 | octets[1];
2872
2852
  const group2 = octets[2] << 8 | octets[3];
2873
2853
  const fullAddress = `${ipv6Part}:${group1.toString(16)}:${group2.toString(16)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1561+241c020e",
3
+ "version": "1.0.0-dev.1572+6945a5ef",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",