@tikoci/centrs 0.1.2 → 0.1.3

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/GLOSSARY.txt +71 -0
  3. package/README.md +24 -8
  4. package/SECURITY.md +3 -3
  5. package/commands/api/README.md +27 -9
  6. package/commands/api/examples.md +109 -6
  7. package/commands/check/README.md +371 -53
  8. package/commands/check/examples.md +390 -0
  9. package/commands/devices/README.md +177 -37
  10. package/commands/devices/examples.md +148 -0
  11. package/commands/execute/README.md +39 -4
  12. package/commands/execute/examples.md +90 -6
  13. package/commands/retrieve/README.md +36 -50
  14. package/commands/retrieve/examples.md +40 -9
  15. package/commands/settings/README.md +286 -22
  16. package/commands/settings/examples.md +408 -0
  17. package/commands/terminal/README.md +6 -0
  18. package/commands/transfer/README.md +32 -2
  19. package/commands/transfer/examples.md +66 -0
  20. package/docs/CONSTITUTION.md +36 -11
  21. package/docs/MATRIX.md +68 -172
  22. package/docs/errors/README.md +2 -1
  23. package/docs/errors/cdb/empty-selection.md +19 -0
  24. package/docs/errors/input/incomplete-gps.md +9 -0
  25. package/docs/errors/input/invalid-altitude.md +9 -0
  26. package/docs/errors/input/invalid-bbox.md +9 -0
  27. package/docs/errors/input/invalid-coordinate.md +9 -0
  28. package/docs/errors/input/invalid-radius.md +9 -0
  29. package/docs/errors/internal/settings-failed.md +9 -0
  30. package/docs/errors/settings/consequential-value.md +26 -0
  31. package/docs/errors/settings/invalid-boolean.md +21 -3
  32. package/docs/errors/settings/invalid-format.md +18 -3
  33. package/docs/errors/settings/invalid-integer.md +18 -3
  34. package/docs/errors/settings/invalid-timeout.md +20 -3
  35. package/docs/errors/settings/invalid-via.md +22 -3
  36. package/docs/errors/settings/reserved-key.md +29 -0
  37. package/docs/errors/settings/skip-env-file-active.md +25 -0
  38. package/docs/errors/settings/unknown-key.md +25 -0
  39. package/docs/errors/usage/fanout-not-supported.md +16 -0
  40. package/package.json +6 -6
  41. package/src/api-fanout.ts +454 -0
  42. package/src/api.ts +91 -19
  43. package/src/cli/api.ts +135 -11
  44. package/src/cli/btest.ts +20 -2
  45. package/src/cli/common.ts +11 -0
  46. package/src/cli/devices.ts +235 -6
  47. package/src/cli/discover.ts +3 -1
  48. package/src/cli/execute.ts +107 -6
  49. package/src/cli/mcp.ts +8 -1
  50. package/src/cli/retrieve.ts +135 -90
  51. package/src/cli/selection.ts +200 -0
  52. package/src/cli/settings.ts +286 -0
  53. package/src/cli/terminal.ts +22 -7
  54. package/src/cli/transfer.ts +171 -47
  55. package/src/cli.ts +7 -0
  56. package/src/core/error-catalog.ts +55 -0
  57. package/src/core/fanout.ts +143 -2
  58. package/src/devices.ts +212 -20
  59. package/src/discover.ts +7 -2
  60. package/src/execute-fanout.ts +438 -0
  61. package/src/execute.ts +120 -23
  62. package/src/index.ts +105 -1
  63. package/src/mcp/config.ts +12 -2
  64. package/src/mcp/tools.ts +137 -2
  65. package/src/protocols/adapter.ts +38 -18
  66. package/src/protocols/index.ts +11 -5
  67. package/src/resolver/cdb.ts +10 -2
  68. package/src/resolver/comment-kv.ts +58 -4
  69. package/src/resolver/config-file.ts +136 -0
  70. package/src/resolver/facts.ts +78 -0
  71. package/src/resolver/geo.ts +453 -0
  72. package/src/resolver/index.ts +46 -0
  73. package/src/resolver/mac.ts +12 -3
  74. package/src/resolver/selection.ts +304 -0
  75. package/src/resolver/settings.ts +54 -5
  76. package/src/resolver/target.ts +20 -2
  77. package/src/retrieve-fanout.ts +225 -133
  78. package/src/retrieve.ts +39 -7
  79. package/src/settings.ts +1078 -0
  80. package/src/terminal.ts +11 -1
  81. package/src/transfer-fanout.ts +527 -0
  82. package/src/transfer.ts +61 -11
package/src/devices.ts CHANGED
@@ -38,10 +38,24 @@ import {
38
38
  applyCommentKv,
39
39
  type CommentKvUpdate,
40
40
  commentKvAllowlist,
41
+ commentKvGeoKeys,
41
42
  commentKvLookupKeys,
42
43
  commentKvReservedKeys,
43
44
  parseCommentKv,
45
+ parseRawCommentFacts,
44
46
  } from "./resolver/comment-kv.ts";
47
+ import { entryFacts, entryLocation, matchesWhere } from "./resolver/facts.ts";
48
+ import {
49
+ type BboxPredicate,
50
+ type DeviceLocation,
51
+ deviceLocation,
52
+ matchesBbox,
53
+ matchesNear,
54
+ type NearPredicate,
55
+ parseAltitude,
56
+ parseAltitudeType,
57
+ parseLatLon,
58
+ } from "./resolver/geo.ts";
45
59
  import { normalizeMac } from "./resolver/mac.ts";
46
60
 
47
61
  export type { SettingSource, SettingSourceKind };
@@ -66,6 +80,8 @@ export interface DevicesListItem {
66
80
  cdbRecordIndex: number;
67
81
  source?: string;
68
82
  sources?: Record<string, SettingSource>;
83
+ /** GPS location, parsed from the `lat`/`lon`/`altitude`/`altitude-type` comment facts (issue #146). */
84
+ location?: DeviceLocation;
69
85
  }
70
86
 
71
87
  export interface DevicesShowItem extends DevicesListItem {
@@ -126,6 +142,13 @@ export interface LoadCdbOptions {
126
142
  cdbFile?: string;
127
143
  cdbPassword?: string;
128
144
  env?: Record<string, string | undefined>;
145
+ /**
146
+ * The `centrs.env` config tier (`src/resolver/config-file.ts`). Only
147
+ * `CENTRS_CDB_FILE` reads it — `CENTRS_CDB_PASSWORD` deliberately never
148
+ * does, same reasoning as `CENTRS_USERNAME`/`CENTRS_PASSWORD` in
149
+ * `resolver/target.ts`'s `resolveAuth`.
150
+ */
151
+ config?: Record<string, string | undefined>;
129
152
  }
130
153
 
131
154
  export interface LoadedCdb {
@@ -154,6 +177,11 @@ export function resolveDevicesSettings(
154
177
  const env = options.env ?? {};
155
178
  let cdbFileSource: SettingSource = { kind: "default", key: "default" };
156
179
  let cdbFile = defaultCdbPath(env);
180
+ const configCdbFile = options.config?.[ENV_CDB_FILE];
181
+ if (configCdbFile) {
182
+ cdbFile = configCdbFile;
183
+ cdbFileSource = { kind: "config", key: ENV_CDB_FILE };
184
+ }
157
185
  const envCdbFile = env[ENV_CDB_FILE];
158
186
  if (envCdbFile) {
159
187
  cdbFile = envCdbFile;
@@ -197,10 +225,12 @@ function isAlreadyExistsError(cause: unknown): boolean {
197
225
  * Create an empty open CDB at `path` without clobbering an existing file.
198
226
  * Uses an exclusive-create open (`wx`) so a concurrent writer's CDB is never
199
227
  * overwritten; the caller treats EEXIST as "another process created it".
228
+ * Mode `0o600` keeps the CDB (device credentials) unreadable by other local
229
+ * users instead of falling back to the process umask default.
200
230
  */
201
231
  async function createEmptyCdbNoClobber(path: string): Promise<void> {
202
232
  await mkdir(dirname(path), { recursive: true });
203
- const handle = await open(path, "wx");
233
+ const handle = await open(path, "wx", 0o600);
204
234
  try {
205
235
  await handle.write(encodeOpenWinBoxCdb([]));
206
236
  await handle.sync();
@@ -434,9 +464,21 @@ function credentialsMissingTips(
434
464
  ];
435
465
  }
436
466
 
467
+ /** One `--where attr=value` device-class clause for `devices list` (exact-match, AND-combined). */
468
+ export interface DevicesWhereClause {
469
+ key: string;
470
+ value: string;
471
+ }
472
+
437
473
  export interface ListDevicesArgs {
438
474
  cdb: LoadedCdb;
439
475
  group?: string;
476
+ /** Repeatable `--where attr=value`, AND-combined (constitution: target selection grammar). */
477
+ where?: readonly DevicesWhereClause[];
478
+ /** `--near <lat>,<lon>,<radius>`: keep devices whose GPS is within radius. */
479
+ near?: NearPredicate;
480
+ /** `--bbox <south>,<west>,<north>,<east>`: keep devices whose GPS is in the box. */
481
+ bbox?: BboxPredicate;
440
482
  }
441
483
 
442
484
  export function listDevices(
@@ -446,18 +488,27 @@ export function listDevices(
446
488
  let entries = args.cdb.entries;
447
489
  let indices = entries.map((_, index) => index);
448
490
 
449
- if (args.group !== undefined) {
450
- const wanted = args.group;
491
+ // Each selector narrows the working (entries, indices) pair (AND-combined —
492
+ // `devices list` is a *filter* surface, distinct from the fan-out resolver's
493
+ // union). `--where` uses the shared `entryFacts`/`matchesWhere` from
494
+ // `src/resolver/facts.ts`, so the list filter and the fan-out selector match
495
+ // against identical device-class facts.
496
+ const narrow = (keep: (entry: WinBoxCdbEntry) => boolean): void => {
451
497
  const filtered: { entry: WinBoxCdbEntry; index: number }[] = [];
452
- for (let index = 0; index < entries.length; index += 1) {
453
- const entry = entries[index];
454
- if (entry && entry.group === wanted) {
455
- filtered.push({ entry, index });
498
+ for (let position = 0; position < entries.length; position += 1) {
499
+ const entry = entries[position];
500
+ if (entry && keep(entry)) {
501
+ filtered.push({ entry, index: indices[position] ?? -1 });
456
502
  }
457
503
  }
458
504
  entries = filtered.map((item) => item.entry);
459
505
  indices = filtered.map((item) => item.index);
460
- if (filtered.length === 0) {
506
+ };
507
+
508
+ if (args.group !== undefined) {
509
+ const wanted = args.group;
510
+ narrow((entry) => entry.group === wanted);
511
+ if (entries.length === 0) {
461
512
  warnings.push({
462
513
  code: "cdb/empty-group",
463
514
  message: `No entries matched group "${wanted}".`,
@@ -466,6 +517,30 @@ export function listDevices(
466
517
  }
467
518
  }
468
519
 
520
+ if (args.where && args.where.length > 0) {
521
+ const where = args.where;
522
+ narrow((entry) =>
523
+ matchesWhere(entryFacts(entry.comment, entry.target, entry.group), where),
524
+ );
525
+ }
526
+
527
+ if (args.near !== undefined || args.bbox !== undefined) {
528
+ const { near, bbox } = args;
529
+ // Geo predicates union among themselves (near OR bbox, like the fan-out
530
+ // resolver), then AND-compose with group/where above. A geo-less device
531
+ // carries no location and is silently excluded (not an error).
532
+ narrow((entry) => {
533
+ const loc = entryLocation(entry.comment);
534
+ if (loc === undefined) {
535
+ return false;
536
+ }
537
+ return (
538
+ (near !== undefined && matchesNear(loc, near)) ||
539
+ (bbox !== undefined && matchesBbox(loc, bbox))
540
+ );
541
+ });
542
+ }
543
+
469
544
  const data: DevicesListItem[] = entries.map((entry, position) =>
470
545
  entryToListItem(entry, indices[position] ?? -1),
471
546
  );
@@ -677,6 +752,10 @@ function entryToListItem(
677
752
  key: `record:${cdbRecordIndex}:source`,
678
753
  };
679
754
  }
755
+ const location = deviceLocation(parseRawCommentFacts(entry.comment));
756
+ if (location !== undefined) {
757
+ item.location = location;
758
+ }
680
759
  return item;
681
760
  }
682
761
 
@@ -805,7 +884,7 @@ function entryToShowItem(
805
884
  entry: WinBoxCdbEntry,
806
885
  cdbRecordIndex: number,
807
886
  ): DevicesShowItem {
808
- return {
887
+ const item: DevicesShowItem = {
809
888
  target: entry.target,
810
889
  recordType: entry.recordType,
811
890
  recordTypeName: recordTypeName(entry.recordType),
@@ -821,6 +900,11 @@ function entryToShowItem(
821
900
  flags: entry.flags,
822
901
  cdbRecordIndex,
823
902
  };
903
+ const location = deviceLocation(parseRawCommentFacts(entry.comment));
904
+ if (location !== undefined) {
905
+ item.location = location;
906
+ }
907
+ return item;
824
908
  }
825
909
 
826
910
  export interface ListGroupsArgs {
@@ -1094,6 +1178,7 @@ async function persistRecords(
1094
1178
  const ALLOWLIST_SET = new Set<string>(commentKvAllowlist);
1095
1179
  const RESERVED_SET = new Set<string>(commentKvReservedKeys);
1096
1180
  const LOOKUP_SET = new Set<string>(commentKvLookupKeys);
1181
+ const GEO_SET = new Set<string>(commentKvGeoKeys);
1097
1182
 
1098
1183
  /**
1099
1184
  * The `usage/not-implemented` error raised by `devices edit`, whose interactive
@@ -1110,19 +1195,92 @@ export function editInteractiveOnlyError(): CentrsError {
1110
1195
  });
1111
1196
  }
1112
1197
 
1198
+ /**
1199
+ * Validate + (for the one enum field) normalize a single geo update's value.
1200
+ * `lat`/`lon`/`altitude` are validated but returned verbatim (stored exactly
1201
+ * as typed); `altitude-type` is validated AND canonicalized to upper case,
1202
+ * since it is a closed two-value enum rather than a freeform/numeric field
1203
+ * (see `src/resolver/geo.ts`). Throws `input/invalid-coordinate` /
1204
+ * `input/invalid-altitude` on a bad value; a `value: null` removal passes
1205
+ * through untouched (nothing to validate).
1206
+ */
1207
+ function normalizeGeoUpdate(update: CommentKvUpdate): CommentKvUpdate {
1208
+ if (update.value === null) {
1209
+ return update;
1210
+ }
1211
+ switch (update.key) {
1212
+ case "lat":
1213
+ parseLatLon(update.value, "lat");
1214
+ return update;
1215
+ case "lon":
1216
+ parseLatLon(update.value, "lon");
1217
+ return update;
1218
+ case "altitude":
1219
+ parseAltitude(update.value);
1220
+ return update;
1221
+ case "altitude-type":
1222
+ return { key: update.key, value: parseAltitudeType(update.value) };
1223
+ default:
1224
+ return update;
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * `lat`/`lon` must land together: setting one without the other, when the
1230
+ * other is not already on the record, is `input/incomplete-gps` (also catches
1231
+ * removing one half of an existing pair). Checked against the MERGE of
1232
+ * `baseComment` (the record's existing comment for `set`, or the base
1233
+ * `--comment` for `add`) and `updates`, so an already-complete record editing
1234
+ * an unrelated key never trips this.
1235
+ */
1236
+ function validateGeoPairing(
1237
+ baseComment: string,
1238
+ updates: readonly CommentKvUpdate[],
1239
+ target: string,
1240
+ ): void {
1241
+ const priorFacts = parseRawCommentFacts(baseComment);
1242
+ let lat = priorFacts["lat"];
1243
+ let lon = priorFacts["lon"];
1244
+ for (const update of updates) {
1245
+ if (update.key === "lat") {
1246
+ lat = update.value === null ? undefined : update.value;
1247
+ } else if (update.key === "lon") {
1248
+ lon = update.value === null ? undefined : update.value;
1249
+ }
1250
+ }
1251
+ const hasLat = lat !== undefined;
1252
+ const hasLon = lon !== undefined;
1253
+ if (hasLat === hasLon) {
1254
+ return;
1255
+ }
1256
+ throw new CentrsError({
1257
+ code: "input/incomplete-gps",
1258
+ summary: `Target "${target}" would end up with only ${hasLat ? "lat" : "lon"} set; lat and lon must be set together.`,
1259
+ remediation:
1260
+ "Pass both --lat and --lon (or lat=... and lon=... together), or use --gps <lat>,<lon>[,<altitude>[,<altitude-type>]] to set both at once.",
1261
+ context: { target, hasLat, hasLon },
1262
+ });
1263
+ }
1264
+
1113
1265
  /**
1114
1266
  * Validate the `k=v` comment positionals shared by `add` and `set`. First-class
1115
1267
  * CDB fields are reserved and throw `cdb/reserved-key` (use the matching flag);
1116
- * tokens outside the recognized override + lookup keys throw `cdb/unknown-option`
1117
- * under `strict`, else append a `cdb/unknown-option` warning. Recognized keys
1118
- * (allowlist overrides + `identity`/`mac`/`ip` lookups) pass silently.
1268
+ * tokens outside the recognized override + lookup + geo keys throw
1269
+ * `cdb/unknown-option` under `strict`, else append a `cdb/unknown-option`
1270
+ * warning. Recognized keys (allowlist overrides, `identity`/`mac`/`ip`
1271
+ * lookups, `lat`/`lon`/`altitude`/`altitude-type` geo facts) pass silently.
1272
+ * Geo values are further validated (range/enum) and checked for `lat`/`lon`
1273
+ * pairing via `src/resolver/geo.ts`; returns the updates with `altitude-type`
1274
+ * normalized to its canonical upper-case form (the only geo field that is not
1275
+ * stored verbatim — see `normalizeGeoUpdate`).
1119
1276
  */
1120
1277
  function validateCommentKvUpdates(
1121
1278
  updates: readonly CommentKvUpdate[],
1122
1279
  target: string,
1123
1280
  strict: boolean | undefined,
1124
1281
  warnings: DevicesWarning[],
1125
- ): void {
1282
+ baseComment = "",
1283
+ ): CommentKvUpdate[] {
1126
1284
  const reserved = updates
1127
1285
  .map((update) => update.key)
1128
1286
  .filter((key) => RESERVED_SET.has(key));
@@ -1130,12 +1288,17 @@ function validateCommentKvUpdates(
1130
1288
  throw new CentrsError({
1131
1289
  code: "cdb/reserved-key",
1132
1290
  summary: `Comment kv-soup cannot carry first-class CDB field(s): ${reserved.join(", ")}.`,
1133
- remediation: `Set ${reserved.join(", ")} with the matching --flag; the comment kv-soup is for ${commentKvAllowlist.join(", ")} plus identity/mac/ip.`,
1291
+ remediation: `Set ${reserved.join(", ")} with the matching --flag; the comment kv-soup is for ${commentKvAllowlist.join(", ")} plus identity/mac/ip/${commentKvGeoKeys.join("/")}.`,
1134
1292
  context: { target, reservedKeys: reserved },
1135
1293
  });
1136
1294
  }
1295
+
1296
+ const normalized = updates.map((update) =>
1297
+ GEO_SET.has(update.key) ? normalizeGeoUpdate(update) : update,
1298
+ );
1299
+
1137
1300
  for (const key of updates.map((update) => update.key)) {
1138
- if (ALLOWLIST_SET.has(key) || LOOKUP_SET.has(key)) {
1301
+ if (ALLOWLIST_SET.has(key) || LOOKUP_SET.has(key) || GEO_SET.has(key)) {
1139
1302
  continue;
1140
1303
  }
1141
1304
  if (strict) {
@@ -1152,6 +1315,17 @@ function validateCommentKvUpdates(
1152
1315
  context: { target, key },
1153
1316
  });
1154
1317
  }
1318
+
1319
+ // Only enforce lat/lon pairing when THIS update actually touches a coordinate
1320
+ // (add or remove). Otherwise an unrelated `set timeout=…` on a record that
1321
+ // already carries a legacy/hand-edited incomplete pair would spuriously fail
1322
+ // with `input/incomplete-gps`; leave a pre-existing pair untouched.
1323
+ if (
1324
+ normalized.some((update) => update.key === "lat" || update.key === "lon")
1325
+ ) {
1326
+ validateGeoPairing(baseComment, normalized, target);
1327
+ }
1328
+ return normalized;
1155
1329
  }
1156
1330
 
1157
1331
  export interface AddDeviceArgs {
@@ -1192,8 +1366,13 @@ export async function addDevice(
1192
1366
  }
1193
1367
 
1194
1368
  const warnings: DevicesWarning[] = [...args.cdb.warnings];
1195
- const updates = args.commentKvUpdates ?? [];
1196
- validateCommentKvUpdates(updates, args.target, args.strict, warnings);
1369
+ const updates = validateCommentKvUpdates(
1370
+ args.commentKvUpdates ?? [],
1371
+ args.target,
1372
+ args.strict,
1373
+ warnings,
1374
+ args.comment ?? "",
1375
+ );
1197
1376
  const comment =
1198
1377
  updates.length > 0 || args.comment !== undefined
1199
1378
  ? applyCommentKv(args.comment ?? "", updates)
@@ -1281,11 +1460,16 @@ export async function setDevice(
1281
1460
  args: SetDeviceArgs,
1282
1461
  ): Promise<DevicesMutationEnvelope> {
1283
1462
  const match = requireSingleMatch(args.cdb.entries, args.target, args.match);
1284
- const updates = args.updates ?? [];
1463
+ const prior = match.entry;
1285
1464
  const warnings: DevicesWarning[] = [...args.cdb.warnings];
1286
- validateCommentKvUpdates(updates, args.target, args.strict, warnings);
1465
+ const updates = validateCommentKvUpdates(
1466
+ args.updates ?? [],
1467
+ args.target,
1468
+ args.strict,
1469
+ warnings,
1470
+ prior.comment,
1471
+ );
1287
1472
 
1288
- const prior = match.entry;
1289
1473
  const comment = applyCommentKv(prior.comment, updates);
1290
1474
  const carried = extraFields(prior.record);
1291
1475
 
@@ -1669,6 +1853,14 @@ function renderShowText(lines: string[], data: DevicesShowEnvelopeData): void {
1669
1853
  lines.push(`profile: ${entry.profile || "-"}`);
1670
1854
  lines.push(`session: ${entry.session || "-"}`);
1671
1855
  lines.push(`comment: ${entry.comment || "-"}`);
1856
+ if (entry.location) {
1857
+ const { lat, lon, altitude, altitudeType } = entry.location;
1858
+ const altitudePart =
1859
+ altitude !== undefined
1860
+ ? ` altitude=${altitude}${altitudeType ? ` (${altitudeType})` : ""}`
1861
+ : "";
1862
+ lines.push(`location: lat=${lat} lon=${lon}${altitudePart}`);
1863
+ }
1672
1864
  if (entry.romonAgent) {
1673
1865
  lines.push(`romon-agent: ${entry.romonAgent}`);
1674
1866
  }
package/src/discover.ts CHANGED
@@ -50,6 +50,11 @@ export const DISCOVER_DEFAULT_REFRESH_INTERVAL_MS = 5_000;
50
50
  /** Default CDB group assigned to saved neighbors. */
51
51
  export const DISCOVER_DEFAULT_GROUP = "discovered";
52
52
 
53
+ /** MNDP uses SO_REUSEPORT where available; Windows does not support it. */
54
+ export function defaultMndpReusePort(platform = process.platform): boolean {
55
+ return platform !== "win32";
56
+ }
57
+
53
58
  export interface ListenMndpOptions {
54
59
  /** Listen window in ms before the listener resolves. Default 15000. */
55
60
  timeoutMs?: number;
@@ -69,7 +74,7 @@ export interface ListenMndpOptions {
69
74
  refreshIntervalMs?: number;
70
75
  /** Enable `SO_REUSEADDR`. Default true. */
71
76
  reuseAddr?: boolean;
72
- /** Enable `SO_REUSEPORT` for coexistence with other MNDP listeners. Default true. */
77
+ /** Enable `SO_REUSEPORT` for coexistence with other MNDP listeners. Default true except on Windows. */
73
78
  reusePort?: boolean;
74
79
  /** Called once with the actually-bound port (useful with `port: 0`). */
75
80
  onBound?: (port: number) => void;
@@ -180,7 +185,7 @@ export function listenMndp(
180
185
  const socket = createSocket({
181
186
  type: "udp4",
182
187
  reuseAddr: options.reuseAddr ?? true,
183
- reusePort: options.reusePort ?? true,
188
+ reusePort: options.reusePort ?? defaultMndpReusePort(),
184
189
  });
185
190
  let settled = false;
186
191
  let boundPort = requestedPort;