signalk-edge-link 2.6.0 → 2.6.1

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/README.md CHANGED
@@ -22,6 +22,9 @@ It is designed for links where latency, packet loss, and bandwidth usage matter
22
22
  - congestion control
23
23
  - optional primary/backup bonding
24
24
  - monitoring and alerting endpoints
25
+ - values snapshot replay on subscribe, retry, and socket recovery
26
+ - optional server-triggered full-state request on restart (`requestFullStatusOnRestart`)
27
+ - Signal K path metadata transport (units, descriptions, zones)
25
28
  - **Multi-connection support** on one Signal K instance
26
29
 
27
30
  ## How data flows
@@ -110,11 +113,11 @@ Check that:
110
113
 
111
114
  ## Protocol version guidance
112
115
 
113
- | Version | Use when | Notes |
114
- | ------- | --------------------------------------------------------- | --------------------------------------------------------------------------- |
115
- | v1 | stable local links, simplest setup | lower overhead, no ACK/NAK reliability layer |
116
- | v2 | packet loss, variable latency, WAN links | adds retransmission, congestion control, bonding, richer monitoring |
117
- | v3 | same use cases as v2 when both peers can upgrade together | keeps v2 features and authenticates ACK/NAK/HEARTBEAT/HELLO control packets |
116
+ | Version | Use when | Notes |
117
+ | ------- | --------------------------------------------------------- | ----------------------------------------------------------------------------- |
118
+ | v1 | stable local links, simplest setup | lower overhead, no ACK/NAK reliability, no metadata transport |
119
+ | v2 | packet loss, variable latency, WAN links | adds retransmission, congestion control, bonding, metadata, richer monitoring |
120
+ | v3 | same use cases as v2 when both peers can upgrade together | keeps v2 features and authenticates ACK/NAK/HEARTBEAT/HELLO control packets |
118
121
 
119
122
  For unstable links, start with **v3** when both peers support it; fall back to **v2** only when you need compatibility with an already deployed v2 peer.
120
123
 
@@ -137,7 +140,7 @@ Most used endpoints:
137
140
  - `GET /bonding`
138
141
  - `POST /bonding`
139
142
 
140
- For full endpoint details, use `docs/api-reference.md
143
+ For full endpoint details, use `docs/api-reference.md`
141
144
 
142
145
  ## Configuration model (summary)
143
146
 
@@ -151,7 +154,8 @@ Configuration is an array of independent connections:
151
154
  "serverType": "server",
152
155
  "udpPort": 4446,
153
156
  "secretKey": "<32-byte key>",
154
- "protocolVersion": 3
157
+ "protocolVersion": 3,
158
+ "requestFullStatusOnRestart": false
155
159
  },
156
160
  {
157
161
  "name": "sat-client",
@@ -168,6 +172,7 @@ Configuration is an array of independent connections:
168
172
  - Each connection runs independently.
169
173
  - Legacy single-object config is auto-normalized to one connection.
170
174
  - Client runtime JSON files (`delta_timer.json`, `subscription.json`, `sentence_filter.json`) are stored per connection and can be edited via API.
175
+ - `requestFullStatusOnRestart` (server mode, v2/v3, default `false`): when enabled, the server sends a `FULL_STATUS_REQUEST` to each client on first contact after a (re)start; the client immediately replays its complete values snapshot so the server rebuilds state without waiting for incremental deltas. Client-side rate-limited to 10 s to prevent replay floods across rapid restarts.
171
176
 
172
177
  For complete setting definitions and ranges, use `docs/configuration-reference.md`.
173
178
 
@@ -199,6 +204,24 @@ Common checks:
199
204
  - Confirm server UDP port is reachable and not already in use.
200
205
  - If link quality is poor, switch to `protocolVersion: 3` when both peers can upgrade together, or `2` if you must stay compatible with an existing v2 peer.
201
206
 
207
+ **`testAddress is only supported on v1 clients` after upgrading to v2/v3**
208
+
209
+ The fields `testAddress`, `testPort`, and `pingIntervalTime` belong to the v1 ping monitor and are not used by v2/v3 clients (which derive RTT from HEARTBEAT exchanges instead). If these fields are present in a connection with `protocolVersion: 2` or `3` the validator will reject the config.
210
+
211
+ Remove them from the affected connection:
212
+
213
+ ```json
214
+ {
215
+ "name": "my-client",
216
+ "serverType": "client",
217
+ "protocolVersion": 3,
218
+ "udpAddress": "...",
219
+ "heartbeatInterval": 25000
220
+ }
221
+ ```
222
+
223
+ The plugin strips these fields automatically on startup, but if you see the error when saving via the SignalK admin UI you need to remove them from the stored config JSON manually once.
224
+
202
225
  For issue-oriented diagnostics, use `docs/troubleshooting.md`.
203
226
 
204
227
  ## Developer commands
@@ -264,7 +287,7 @@ window.__EDGE_LINK_AUTH__ = {
264
287
  - `docs/README.md` (documentation index)
265
288
  - `docs/architecture-overview.md` (system architecture and lifecycle)
266
289
  - `docs/configuration-reference.md` (settings and defaults)
267
- - `docs/api-reference.md
290
+ - `docs/api-reference.md`
268
291
  - `docs/protocol-v2.md` (reliable protocol operational overview)
269
292
  - `docs/protocol-v3-spec.md` (authenticated control-plane details)
270
293
  - `docs/bonding.md` (bonding concepts and API usage)
@@ -14,7 +14,6 @@ exports.VALID_CONNECTION_KEYS = [
14
14
  "name",
15
15
  "serverType",
16
16
  "udpPort",
17
- "udpMetaPort",
18
17
  "secretKey",
19
18
  "stretchAsciiKey",
20
19
  "useMsgpack",
@@ -28,6 +27,7 @@ exports.VALID_CONNECTION_KEYS = [
28
27
  "testAddress",
29
28
  "testPort",
30
29
  "pingIntervalTime",
30
+ "requestFullStatusOnRestart",
31
31
  "reliability",
32
32
  "congestionControl",
33
33
  "bonding",
@@ -85,9 +85,6 @@ function validateConnectionConfig(connection, prefix = "") {
85
85
  if (!isValidPort(conn.udpPort, 1024)) {
86
86
  return `${p}udpPort must be an integer between 1024 and 65535`;
87
87
  }
88
- if (conn.udpMetaPort !== undefined && !isValidPort(conn.udpMetaPort, 1024)) {
89
- return `${p}udpMetaPort must be an integer between 1024 and 65535`;
90
- }
91
88
  try {
92
89
  (0, crypto_1.validateSecretKey)(conn.secretKey);
93
90
  }
package/lib/instance.js CHANGED
@@ -69,7 +69,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
69
69
  isHealthy: false,
70
70
  options,
71
71
  socketUdp: null,
72
- metaSocketUdp: null,
73
72
  readyToSend: false,
74
73
  stopped: false,
75
74
  isServerMode: false,
@@ -252,16 +251,8 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
252
251
  if (entries.length === 0) {
253
252
  return false;
254
253
  }
255
- const protoVer = options.protocolVersion ?? 2;
256
254
  try {
257
- if (protoVer === 1) {
258
- if (!options.udpMetaPort || options.udpMetaPort <= 0) {
259
- app.debug(`[${instanceId}] Meta skipped: v1 pipeline requires 'udpMetaPort' in connection config`);
260
- return false;
261
- }
262
- await getV1Pipeline().packCryptMeta(entries, kind, options.secretKey, options.udpAddress, options.udpMetaPort);
263
- }
264
- else if (state.pipeline && typeof state.pipeline.sendMetadata === "function") {
255
+ if (state.pipeline && typeof state.pipeline.sendMetadata === "function") {
265
256
  await state.pipeline.sendMetadata(entries, kind, options.secretKey, options.udpAddress, options.udpPort);
266
257
  }
267
258
  else {
@@ -874,34 +865,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
874
865
  getV1Pipeline().unpackDecrypt(delta, options.secretKey);
875
866
  });
876
867
  app.debug(`[${instanceId}] [v1] Server pipeline initialized`);
877
- // v1 has no packet-type byte, so meta is streamed on a separate UDP
878
- // port by the client. Bind that port here when the operator has opted
879
- // in. If `udpMetaPort` is unset we simply don't listen — keeping the
880
- // receive side idle is the correct default for existing v1 peers that
881
- // don't know about meta.
882
- if (typeof options.udpMetaPort === "number" && options.udpMetaPort > 0) {
883
- if (options.udpMetaPort === options.udpPort) {
884
- app.error(`[${instanceId}] [v1] udpMetaPort (${options.udpMetaPort}) must differ from udpPort; meta disabled`);
885
- }
886
- else {
887
- const metaSocket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
888
- state.metaSocketUdp = metaSocket;
889
- metaSocket.on("message", (msg) => {
890
- getV1Pipeline()
891
- .unpackDecryptMeta(msg, options.secretKey)
892
- .catch((err) => {
893
- const m = err instanceof Error ? err.message : String(err);
894
- app.debug(`[${instanceId}] [v1] meta decrypt failed: ${m}`);
895
- });
896
- });
897
- metaSocket.on("error", (err) => {
898
- app.error(`[${instanceId}] [v1] meta socket error: ${err.message} (${err.code})`);
899
- recordError("udpSend", `v1 meta socket error: ${err.message} (${err.code})`);
900
- });
901
- metaSocket.bind(options.udpMetaPort);
902
- app.debug(`[${instanceId}] [v1] Meta listener bound to UDP :${options.udpMetaPort}`);
903
- }
904
- }
905
868
  }
906
869
  const startupSocket = state.socketUdp;
907
870
  await new Promise((resolve, reject) => {
@@ -1312,16 +1275,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
1312
1275
  state.socketUdp = null;
1313
1276
  app.debug(`[${instanceId}] Stopped`);
1314
1277
  }
1315
- if (state.metaSocketUdp) {
1316
- try {
1317
- state.metaSocketUdp.close();
1318
- }
1319
- catch (err) {
1320
- const msg = err instanceof Error ? err.message : String(err);
1321
- app.debug(`[${instanceId}] Meta socket close failed: ${msg}`);
1322
- }
1323
- state.metaSocketUdp = null;
1324
- }
1325
1278
  _setStatus("Stopped", false);
1326
1279
  }
1327
1280
  // ── Public API ────────────────────────────────────────────────────────────
package/lib/pipeline.js CHANGED
@@ -39,15 +39,6 @@ const delta_sanitizer_1 = require("./delta-sanitizer");
39
39
  const source_dispatch_1 = require("./source-dispatch");
40
40
  const pipeline_utils_1 = require("./pipeline-utils");
41
41
  const constants_1 = require("./constants");
42
- const metadata_1 = require("./metadata");
43
- /** Leading magic that distinguishes v1 meta payloads from v1 deltas, placed
44
- * inside the encrypted plaintext so existing v1 receivers (which do not
45
- * recognise it) simply reject the packet rather than misinterpreting it. */
46
- const V1_META_MAGIC = Buffer.from("SKM1", "ascii");
47
- /** Threshold for v1 sender-restart detection — see the v2 server's
48
- * META_RESTART_THRESHOLD comment. envSeq=0 is treated as a restart only when
49
- * the last accepted seq has moved beyond this small reorder window. */
50
- const META_RESTART_THRESHOLD_V1 = 8;
51
42
  /**
52
43
  * Creates the data processing pipeline (compress, encrypt, send / receive, decrypt, decompress).
53
44
  * @param app - SignalK app object (for logging)
@@ -58,12 +49,6 @@ const META_RESTART_THRESHOLD_V1 = 8;
58
49
  function createPipeline(app, state, metricsApi) {
59
50
  const { metrics, recordError, trackPathStats } = metricsApi;
60
51
  const setStatus = app.setPluginStatus || app.setProviderStatus || (() => { });
61
- let metaEnvelopeSeqV1 = 0;
62
- // Last accepted inner-envelope seq on the receive side. v1 has no
63
- // per-session concept (one socket per pipeline instance), so a single
64
- // closure variable is sufficient. Used to drop stale/duplicate envelopes
65
- // that UDP reorders or replays.
66
- let lastIngestedMetaEnvSeqV1 = null;
67
52
  /**
68
53
  * Compresses, encrypts, and sends delta data via UDP.
69
54
  * Pipeline: Serialize -> Compress -> Encrypt (AES-256-GCM) -> Send
@@ -148,73 +133,6 @@ function createPipeline(app, state, metricsApi) {
148
133
  }
149
134
  }
150
135
  }
151
- /**
152
- * Sends Signal K path metadata to the receiver using the v1 wire format on a
153
- * separate UDP port.
154
- *
155
- * v1 has no packet-type byte so we cannot multiplex meta with deltas on the
156
- * existing port without breaking every deployed v1 receiver. To keep the
157
- * change backward-compatible, meta is sent on `udpMetaPort` with a 4-byte
158
- * `SKM1` magic prefix inside the encrypted plaintext — a v1 receiver that
159
- * has not been upgraded will fail to JSON-parse the payload and simply drop
160
- * it without side effects.
161
- */
162
- async function packCryptMeta(entries, kind, secretKey, udpAddress, udpMetaPort) {
163
- try {
164
- if (!state.options) {
165
- app.debug("packCryptMeta called but plugin is stopped, ignoring");
166
- return;
167
- }
168
- if (!udpMetaPort || udpMetaPort <= 0) {
169
- app.debug("packCryptMeta: no udpMetaPort configured, meta disabled on v1");
170
- return;
171
- }
172
- if (entries.length === 0) {
173
- return;
174
- }
175
- const usePathDict = !!state.options.usePathDictionary;
176
- const useMsgpack = !!state.options.useMsgpack;
177
- const maxPerPacket = state.metaConfig?.maxPathsPerPacket ?? 500;
178
- const chunks = (0, metadata_1.splitIntoPackets)(entries, maxPerPacket);
179
- const envelopeSeq = metaEnvelopeSeqV1++ >>> 0;
180
- for (let i = 0; i < chunks.length; i++) {
181
- const chunk = chunks[i];
182
- const processed = usePathDict ? chunk.map(pathDictionary_1.encodeMetaEntry) : chunk;
183
- const envelope = (0, metadata_1.buildMetaEnvelope)(processed, kind, envelopeSeq, i, chunks.length);
184
- const serialized = (0, pipeline_utils_1.deltaBuffer)(envelope, useMsgpack);
185
- const withMagic = Buffer.concat([V1_META_MAGIC, serialized]);
186
- const compressed = await (0, pipeline_utils_1.compressPayload)(withMagic, useMsgpack);
187
- const packet = (0, crypto_1.encryptBinary)(compressed, secretKey, {
188
- stretchAsciiKey: !!state.options.stretchAsciiKey
189
- });
190
- if (packet.length > constants_1.MAX_SAFE_UDP_PAYLOAD) {
191
- app.debug(`Warning: v1 meta packet size ${packet.length} bytes exceeds safe MTU (${constants_1.MAX_SAFE_UDP_PAYLOAD})`);
192
- }
193
- await udpSendAsync(packet, udpAddress, udpMetaPort);
194
- metrics.bandwidth.metaBytesOut = (metrics.bandwidth.metaBytesOut || 0) + packet.length;
195
- metrics.bandwidth.metaPacketsOut = (metrics.bandwidth.metaPacketsOut || 0) + 1;
196
- metrics.bandwidth.bytesOut += packet.length;
197
- metrics.bandwidth.packetsOut++;
198
- }
199
- if (kind === "snapshot") {
200
- metrics.bandwidth.metaSnapshotsSent = (metrics.bandwidth.metaSnapshotsSent || 0) + 1;
201
- }
202
- else {
203
- metrics.bandwidth.metaDiffsSent = (metrics.bandwidth.metaDiffsSent || 0) + 1;
204
- }
205
- app.debug(`v1 meta sent: kind=${kind}, entries=${entries.length}, chunks=${chunks.length}, envSeq=${envelopeSeq}`);
206
- }
207
- catch (error) {
208
- const msg = error instanceof Error ? error.message : String(error);
209
- app.error(`packCryptMeta error: ${msg}`);
210
- recordError("general", `packCryptMeta error: ${msg}`);
211
- // Re-throw so the caller (sendMetaEntries) can tell the send failed
212
- // and refrain from committing the MetaCache. Without this, a broken
213
- // socket/encryption/compression would silently suppress every future
214
- // diff for the affected paths.
215
- throw error instanceof Error ? error : new Error(msg);
216
- }
217
- }
218
136
  /**
219
137
  * Decompresses, decrypts, and processes received UDP data.
220
138
  * Pipeline: Receive -> Decrypt (AES-256-GCM) -> Decompress -> Parse -> Process
@@ -357,138 +275,6 @@ function createPipeline(app, state, metricsApi) {
357
275
  }
358
276
  });
359
277
  }
360
- /**
361
- * Receive-side counterpart to `packCryptMeta` for v1. Decrypts a packet
362
- * arrived on `udpMetaPort`, verifies the 4-byte `SKM1` magic inside the
363
- * plaintext (packets without the magic are dropped — v1 has no packet-type
364
- * byte, so the magic is the only signal that this is a meta payload and not
365
- * a corrupted delta), and dispatches each entry as a minimal Signal K delta
366
- * with `updates[].meta[]` via `app.handleMessage`.
367
- */
368
- async function unpackDecryptMeta(packet, secretKey) {
369
- try {
370
- if (!state.options) {
371
- app.debug("unpackDecryptMeta called but plugin is stopped, ignoring");
372
- return;
373
- }
374
- // Bump bytesIn/packetsIn AND the meta-scoped counters at the same
375
- // gate — any packet that reached this code is a meta packet (the
376
- // separate udpMetaPort ensures that), so bytesIn should always equal
377
- // metaBytesIn for this pipeline path. Keeping them in lockstep lets
378
- // consumers cross-check: bytesIn === dataBytesIn + metaBytesIn.
379
- metrics.bandwidth.bytesIn += packet.length;
380
- metrics.bandwidth.packetsIn++;
381
- metrics.bandwidth.metaBytesIn = (metrics.bandwidth.metaBytesIn || 0) + packet.length;
382
- metrics.bandwidth.metaPacketsIn = (metrics.bandwidth.metaPacketsIn || 0) + 1;
383
- const decrypted = (0, crypto_1.decryptBinary)(packet, secretKey, {
384
- stretchAsciiKey: !!state.options.stretchAsciiKey
385
- });
386
- const decompressed = (await (0, pipeline_utils_1.brotliDecompressAsync)(decrypted, {
387
- maxOutputLength: constants_1.MAX_DECOMPRESSED_SIZE
388
- }));
389
- if (decompressed.length < V1_META_MAGIC.length) {
390
- app.debug("v1 meta: decompressed payload too short, ignoring");
391
- return;
392
- }
393
- // Reject anything that isn't prefixed with the SKM1 magic so a stray
394
- // non-meta packet on the meta port (misconfiguration, replay, attacker)
395
- // cannot be misinterpreted. The magic lives INSIDE the encrypted
396
- // plaintext, so this check is authenticated.
397
- if (decompressed.subarray(0, V1_META_MAGIC.length).compare(V1_META_MAGIC) !== 0) {
398
- app.debug("v1 meta: missing SKM1 magic, dropping");
399
- return;
400
- }
401
- const body = decompressed.subarray(V1_META_MAGIC.length);
402
- if (body.length > constants_1.MAX_PARSE_PAYLOAD_SIZE) {
403
- app.error(`v1 meta: payload too large to parse: ${body.length} bytes`);
404
- return;
405
- }
406
- let content;
407
- if (state.options.useMsgpack) {
408
- try {
409
- content = msgpack.decode(body);
410
- }
411
- catch {
412
- content = JSON.parse(body.toString());
413
- }
414
- }
415
- else {
416
- content = JSON.parse(body.toString());
417
- }
418
- if (!content || typeof content !== "object" || Array.isArray(content)) {
419
- app.debug("v1 meta: envelope was not an object, dropping");
420
- return;
421
- }
422
- const env = content;
423
- if (!Array.isArray(env.entries) || env.entries.length === 0) {
424
- return;
425
- }
426
- // Drop stale/duplicate envelopes. The inner envelope `seq` is shared
427
- // across all chunks of the same batch, so equal-seq chunks are still
428
- // accepted; only earlier batches are rejected. Uint32-wrap aware so a
429
- // long-running sender's wrap doesn't trigger mass-rejection.
430
- //
431
- // Sender-restart detection: the v1 client's meta envelope counter
432
- // initialises to 0 at process start. Treat envSeq=0 as a peer restart
433
- // only once lastIngestedMetaEnvSeqV1 has advanced beyond a small
434
- // reorder window — below the threshold, envSeq=0 is ambiguous with
435
- // first-packet replay and falls through to normal dedup.
436
- if (typeof env.seq === "number" && Number.isFinite(env.seq)) {
437
- const envSeq = env.seq >>> 0;
438
- if (lastIngestedMetaEnvSeqV1 !== null &&
439
- envSeq === 0 &&
440
- lastIngestedMetaEnvSeqV1 >= META_RESTART_THRESHOLD_V1) {
441
- app.debug(`v1 meta: sender restart detected (last seq was ${lastIngestedMetaEnvSeqV1}); resetting`);
442
- lastIngestedMetaEnvSeqV1 = null;
443
- }
444
- if (lastIngestedMetaEnvSeqV1 !== null) {
445
- const distance = (envSeq - lastIngestedMetaEnvSeqV1) >>> 0;
446
- if (distance !== 0 && distance >= 0x80000000) {
447
- app.debug(`v1 meta: stale envelope seq=${envSeq} (last=${lastIngestedMetaEnvSeqV1}), dropping`);
448
- return;
449
- }
450
- if (distance !== 0) {
451
- lastIngestedMetaEnvSeqV1 = envSeq;
452
- }
453
- }
454
- else {
455
- lastIngestedMetaEnvSeqV1 = envSeq;
456
- }
457
- }
458
- const nowIso = new Date().toISOString();
459
- const usePathDict = !!state.options.usePathDictionary;
460
- for (const rawEntry of env.entries) {
461
- if (!rawEntry || typeof rawEntry.meta !== "object" || !rawEntry.meta) {
462
- continue;
463
- }
464
- const entry = usePathDict
465
- ? (0, pathDictionary_1.decodeMetaEntry)(rawEntry)
466
- : rawEntry;
467
- const path = typeof entry.path === "string" ? entry.path : String(entry.path ?? "");
468
- if (!path) {
469
- continue;
470
- }
471
- const context = typeof rawEntry.context === "string" ? rawEntry.context : "vessels.self";
472
- const delta = {
473
- context,
474
- updates: [
475
- {
476
- timestamp: nowIso,
477
- values: [],
478
- meta: [{ path, value: entry.meta }]
479
- }
480
- ]
481
- };
482
- app.handleMessage("", delta);
483
- }
484
- app.debug(`v1 meta received: kind=${env.kind ?? "?"}, entries=${env.entries.length}`);
485
- }
486
- catch (error) {
487
- const msg = error instanceof Error ? error.message : String(error);
488
- app.error(`unpackDecryptMeta error: ${msg}`);
489
- recordError("general", `unpackDecryptMeta error: ${msg}`);
490
- }
491
- }
492
- return { packCrypt, packCryptMeta, unpackDecrypt, unpackDecryptMeta };
278
+ return { packCrypt, unpackDecrypt };
493
279
  }
494
280
  module.exports = createPipeline;
@@ -187,6 +187,7 @@ function register(router, ctx) {
187
187
  error: error instanceof Error ? error.message : String(error)
188
188
  });
189
189
  }
190
+ connectionList = connectionList.map((connection) => (0, connection_config_1.sanitizeConnectionConfig)(connection));
190
191
  for (let index = 0; index < connectionList.length; index++) {
191
192
  const prefix = connectionList.length > 1 ? `connections[${index}].` : "";
192
193
  const validationError = (0, connection_config_1.validateConnectionConfig)(connectionList[index], prefix);
@@ -213,7 +214,7 @@ function register(router, ctx) {
213
214
  }
214
215
  }
215
216
  const finalConfig = {
216
- connections: connectionList.map((connection) => (0, connection_config_1.sanitizeConnectionConfig)(connection))
217
+ connections: connectionList
217
218
  };
218
219
  if (resolvedManagementToken !== undefined) {
219
220
  finalConfig.managementApiToken = resolvedManagementToken;
@@ -191,12 +191,13 @@ function register(router, ctx) {
191
191
  if (!body.name) {
192
192
  return res.status(400).json({ error: "Missing required field 'name'" });
193
193
  }
194
- const validationError = (0, connection_config_1.validateConnectionConfig)(body);
194
+ const sanitized = (0, connection_config_1.sanitizeConnectionConfig)(body);
195
+ const validationError = (0, connection_config_1.validateConnectionConfig)(sanitized);
195
196
  if (validationError) {
196
197
  return res.status(400).json({ error: validationError });
197
198
  }
198
199
  const connections = getCurrentConnectionsConfig();
199
- connections.push((0, connection_config_1.sanitizeConnectionConfig)(body));
200
+ connections.push(sanitized);
200
201
  const portError = (0, connection_config_1.validateUniqueServerPorts)(connections);
201
202
  if (portError) {
202
203
  return res.status(400).json({ error: portError });
@@ -237,15 +238,11 @@ function register(router, ctx) {
237
238
  const mutableAllowed = new Set([
238
239
  "name",
239
240
  "protocolVersion",
240
- "udpMetaPort",
241
241
  "useMsgpack",
242
242
  "usePathDictionary",
243
243
  "enableNotifications",
244
244
  "udpAddress",
245
245
  "helloMessageSender",
246
- "testAddress",
247
- "testPort",
248
- "pingIntervalTime",
249
246
  "reliability",
250
247
  "congestionControl",
251
248
  "bonding",
@@ -264,11 +261,12 @@ function register(router, ctx) {
264
261
  }
265
262
  }
266
263
  const mergedConnection = { ...connections[idx], ...patch };
267
- const validationError = (0, connection_config_1.validateConnectionConfig)(mergedConnection);
264
+ const sanitizedConnection = (0, connection_config_1.sanitizeConnectionConfig)(mergedConnection);
265
+ const validationError = (0, connection_config_1.validateConnectionConfig)(sanitizedConnection);
268
266
  if (validationError) {
269
267
  return res.status(400).json({ error: validationError });
270
268
  }
271
- connections[idx] = (0, connection_config_1.sanitizeConnectionConfig)(mergedConnection);
269
+ connections[idx] = sanitizedConnection;
272
270
  const portError = (0, connection_config_1.validateUniqueServerPorts)(connections);
273
271
  if (portError) {
274
272
  return res.status(400).json({ error: portError });
@@ -46,13 +46,6 @@ exports.commonConnectionProperties = {
46
46
  minimum: 1024,
47
47
  maximum: 65535
48
48
  },
49
- udpMetaPort: {
50
- type: "integer",
51
- title: "v1 Metadata UDP Port",
52
- description: "Optional separate UDP port for v1 metadata packets; ignored by v2/v3.",
53
- minimum: 1024,
54
- maximum: 65535
55
- },
56
49
  secretKey: {
57
50
  type: "string",
58
51
  title: "Encryption Key",