@owine/unifi-network-mcp 2.15.0 → 3.1.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -77
  3. package/dist/client.d.ts +26 -1
  4. package/dist/client.js +199 -24
  5. package/dist/config.d.ts +11 -0
  6. package/dist/config.js +102 -3
  7. package/dist/guard.d.ts +46 -0
  8. package/dist/guard.js +180 -0
  9. package/dist/index.js +17 -4
  10. package/dist/instructions.d.ts +9 -0
  11. package/dist/instructions.js +32 -0
  12. package/dist/tls.d.ts +14 -0
  13. package/dist/tls.js +58 -0
  14. package/dist/tools/acl.js +123 -50
  15. package/dist/tools/classic.d.ts +3 -0
  16. package/dist/tools/classic.js +54 -0
  17. package/dist/tools/client-history.js +22 -12
  18. package/dist/tools/clients.js +10 -9
  19. package/dist/tools/devices.js +21 -20
  20. package/dist/tools/dns-policies.js +27 -27
  21. package/dist/tools/exposure.d.ts +3 -0
  22. package/dist/tools/exposure.js +118 -0
  23. package/dist/tools/firewall.d.ts +2 -1
  24. package/dist/tools/firewall.js +200 -75
  25. package/dist/tools/hotspot.js +35 -28
  26. package/dist/tools/index.d.ts +13 -1
  27. package/dist/tools/index.js +67 -4
  28. package/dist/tools/networks.d.ts +2 -1
  29. package/dist/tools/networks.js +44 -31
  30. package/dist/tools/sites.js +2 -2
  31. package/dist/tools/supporting.js +22 -21
  32. package/dist/tools/switching.js +16 -15
  33. package/dist/tools/system.js +1 -1
  34. package/dist/tools/traffic-matching.d.ts +2 -1
  35. package/dist/tools/traffic-matching.js +22 -20
  36. package/dist/tools/wifi.js +76 -30
  37. package/dist/utils/full-put.d.ts +11 -2
  38. package/dist/utils/full-put.js +25 -2
  39. package/dist/utils/output-schemas.d.ts +241 -4
  40. package/dist/utils/output-schemas.js +225 -49
  41. package/dist/utils/params.d.ts +12 -0
  42. package/dist/utils/params.js +21 -0
  43. package/dist/utils/pick.d.ts +6 -0
  44. package/dist/utils/pick.js +11 -0
  45. package/dist/utils/responses.d.ts +2 -0
  46. package/dist/utils/responses.js +28 -2
  47. package/dist/utils/safety.d.ts +35 -1
  48. package/dist/utils/safety.js +53 -3
  49. package/dist/utils/secrets.js +3 -0
  50. package/dist/utils/wan-exposure.d.ts +69 -0
  51. package/dist/utils/wan-exposure.js +245 -0
  52. package/dist/write-log.d.ts +52 -0
  53. package/dist/write-log.js +178 -0
  54. package/dist/write-tools.d.ts +27 -0
  55. package/dist/write-tools.js +50 -0
  56. package/package.json +19 -15
@@ -1,22 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listClientSessionsOutputSchema = exports.listClientHistoryOutputSchema = exports.ClientSessionEnvelope = exports.ClientSession = exports.HistoryClient = exports.listPendingDevicesOutputSchema = exports.listCountriesOutputSchema = exports.listDpiApplicationsOutputSchema = exports.listDpiCategoriesOutputSchema = exports.listDeviceTagsOutputSchema = exports.listRadiusProfilesOutputSchema = exports.listVpnTunnelsOutputSchema = exports.listVpnServersOutputSchema = exports.listWansOutputSchema = exports.getWifiOutputSchema = exports.listWifiOutputSchema = exports.getClientOutputSchema = exports.listClientsOutputSchema = exports.getDeviceStatisticsOutputSchema = exports.getDeviceOutputSchema = exports.listDevicesOutputSchema = exports.listTrafficMatchingListsOutputSchema = exports.trafficMatchingListOutputSchema = exports.listDnsPoliciesOutputSchema = exports.dnsPolicyOutputSchema = exports.listLagsOutputSchema = exports.lagOutputSchema = exports.listMcLagDomainsOutputSchema = exports.mcLagDomainOutputSchema = exports.listSwitchStacksOutputSchema = exports.switchStackOutputSchema = exports.aclRuleOrderingOutputSchema = exports.listAclRulesOutputSchema = exports.aclRuleOutputSchema = exports.firewallPolicyOrderingOutputSchema = exports.listFirewallPoliciesOutputSchema = exports.firewallPolicyOutputSchema = exports.listFirewallZonesOutputSchema = exports.firewallZoneOutputSchema = exports.createVouchersOutputSchema = exports.listVouchersOutputSchema = exports.voucherOutputSchema = exports.getNetworkReferencesOutputSchema = exports.listNetworksOutputSchema = exports.networkOutputSchema = exports.listSitesOutputSchema = exports.getInfoOutputSchema = void 0;
3
+ exports.LegacyFirewallRule = exports.ClassicFirewallGroup = exports.ClassicNetwork = exports.listClientSessionsOutputSchema = exports.listClientHistoryOutputSchema = exports.ClientSessionEnvelope = exports.ClientSession = exports.HistoryClient = exports.listPendingDevicesOutputSchema = exports.listCountriesOutputSchema = exports.listDpiApplicationsOutputSchema = exports.listDpiCategoriesOutputSchema = exports.listDeviceTagsOutputSchema = exports.listRadiusProfilesOutputSchema = exports.listVpnTunnelsOutputSchema = exports.listVpnServersOutputSchema = exports.listWansOutputSchema = exports.getWifiOutputSchema = exports.listWifiOutputSchema = exports.getClientOutputSchema = exports.listClientsOutputSchema = exports.getDeviceStatisticsOutputSchema = exports.getDeviceOutputSchema = exports.listDevicesOutputSchema = exports.listTrafficMatchingListsOutputSchema = exports.trafficMatchingListOutputSchema = exports.listDnsPoliciesOutputSchema = exports.dnsPolicyOutputSchema = exports.listLagsOutputSchema = exports.lagOutputSchema = exports.listMcLagDomainsOutputSchema = exports.mcLagDomainOutputSchema = exports.listSwitchStacksOutputSchema = exports.switchStackOutputSchema = exports.aclRuleOrderingOutputSchema = exports.listAclRulesOutputSchema = exports.aclRuleOutputSchema = exports.firewallPolicyOrderingOutputSchema = exports.listFirewallPoliciesOutputSchema = exports.firewallPolicyOutputSchema = exports.listFirewallZonesOutputSchema = exports.firewallZoneOutputSchema = exports.createVouchersOutputSchema = exports.listVouchersOutputSchema = exports.voucherOutputSchema = exports.getNetworkReferencesOutputSchema = exports.listNetworksOutputSchema = exports.networkOutputSchema = exports.listSitesOutputSchema = exports.getInfoOutputSchema = void 0;
4
+ exports.getDeviceSshSettingsOutputSchema = exports.getUpnpStatusOutputSchema = exports.listPortForwardsOutputSchema = exports.PortForward = exports.PortForwardDestination = exports.listLegacyFirewallRulesOutputSchema = exports.listFirewallGroupsOutputSchema = exports.listClassicNetworksOutputSchema = exports.ClassicEnvelope = void 0;
5
+ exports.looseOutput = looseOutput;
6
+ exports.writeResult = writeResult;
4
7
  /**
5
- * Output schemas for high-value read tools (loose strategy: every non-key
6
- * field optional, every nested object uses .passthrough() to allow
8
+ * Output schemas for every read tool, and for write tools whose response
9
+ * echoes the resource (loose strategy: every non-key
10
+ * field optional, every nested object uses .loose() to allow
7
11
  * firmware/hardware-specific fields to flow through unchanged).
8
12
  *
9
13
  * Verified against UniFi Network API 11.0.83. Where the docs collapse
10
14
  * nested arrays/objects (e.g. interfaces.ports[], radios[]), the schema
11
- * uses passthrough records so the contract doesn't lock to fields we
12
- * haven't verified.
15
+ * uses loose records so the contract doesn't lock to fields we
16
+ * haven't verified. Tools register these raw shapes, and registerAllTools()
17
+ * publishes each as a loose object (src/tools/index.ts), so fields a schema
18
+ * does not list never fail client-side validation.
13
19
  */
14
20
  const zod_1 = require("zod");
15
21
  // configurable is live-verified (11.0.83) on system-defined zones, policies,
16
22
  // networks and RADIUS profiles; it is absent on user-defined resources.
17
23
  const Metadata = zod_1.z
18
24
  .object({ origin: zod_1.z.string().optional(), configurable: zod_1.z.boolean().optional() })
19
- .passthrough();
25
+ .loose();
20
26
  const PaginationFields = {
21
27
  offset: zod_1.z.number().int().optional(),
22
28
  limit: zod_1.z.number().int().optional(),
@@ -45,7 +51,7 @@ const DeviceListItem = zod_1.z
45
51
  features: zod_1.z.array(zod_1.z.string()).optional(),
46
52
  interfaces: zod_1.z.array(zod_1.z.string()).optional(),
47
53
  })
48
- .passthrough();
54
+ .loose();
49
55
  const DevicePort = zod_1.z
50
56
  .object({
51
57
  idx: zod_1.z.number(),
@@ -61,10 +67,10 @@ const DevicePort = zod_1.z
61
67
  enabled: zod_1.z.boolean().optional(),
62
68
  state: zod_1.z.string().optional(),
63
69
  })
64
- .passthrough()
70
+ .loose()
65
71
  .optional(),
66
72
  })
67
- .passthrough();
73
+ .loose();
68
74
  const DeviceRadio = zod_1.z
69
75
  .object({
70
76
  wlanStandard: zod_1.z.string().optional(),
@@ -72,7 +78,7 @@ const DeviceRadio = zod_1.z
72
78
  channelWidthMHz: zod_1.z.number().optional(),
73
79
  channel: zod_1.z.number().optional(),
74
80
  })
75
- .passthrough();
81
+ .loose();
76
82
  const DeviceDetail = zod_1.z
77
83
  .object({
78
84
  ...DeviceCommon,
@@ -81,7 +87,7 @@ const DeviceDetail = zod_1.z
81
87
  configurationId: zod_1.z.string().optional(),
82
88
  uplink: zod_1.z
83
89
  .object({ deviceId: zod_1.z.string().optional() })
84
- .passthrough()
90
+ .loose()
85
91
  .optional(),
86
92
  // Detail view: object keyed by capability (switching/accessPoint).
87
93
  features: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
@@ -90,10 +96,10 @@ const DeviceDetail = zod_1.z
90
96
  ports: zod_1.z.array(DevicePort).optional(),
91
97
  radios: zod_1.z.array(DeviceRadio).optional(),
92
98
  })
93
- .passthrough()
99
+ .loose()
94
100
  .optional(),
95
101
  })
96
- .passthrough();
102
+ .loose();
97
103
  const DeviceStatistics = zod_1.z
98
104
  .object({
99
105
  uptimeSec: zod_1.z.number().optional(),
@@ -109,7 +115,7 @@ const DeviceStatistics = zod_1.z
109
115
  txRateBps: zod_1.z.number().optional(),
110
116
  rxRateBps: zod_1.z.number().optional(),
111
117
  })
112
- .passthrough()
118
+ .loose()
113
119
  .optional(),
114
120
  interfaces: zod_1.z
115
121
  .object({
@@ -119,13 +125,13 @@ const DeviceStatistics = zod_1.z
119
125
  frequencyGHz: zod_1.z.number().optional(),
120
126
  txRetriesPct: zod_1.z.number().optional(),
121
127
  })
122
- .passthrough())
128
+ .loose())
123
129
  .optional(),
124
130
  })
125
- .passthrough()
131
+ .loose()
126
132
  .optional(),
127
133
  })
128
- .passthrough();
134
+ .loose();
129
135
  const Client = zod_1.z
130
136
  .object({
131
137
  id: zod_1.z.string(),
@@ -137,10 +143,10 @@ const Client = zod_1.z
137
143
  uplinkDeviceId: zod_1.z.string().optional(),
138
144
  access: zod_1.z
139
145
  .object({ type: zod_1.z.string().optional() })
140
- .passthrough()
146
+ .loose()
141
147
  .optional(),
142
148
  })
143
- .passthrough();
149
+ .loose();
144
150
  const WifiBroadcast = zod_1.z
145
151
  .object({
146
152
  id: zod_1.z.string(),
@@ -159,20 +165,20 @@ const WifiBroadcast = zod_1.z
159
165
  broadcastingDeviceFilter: zod_1.z.unknown().optional(),
160
166
  metadata: Metadata.optional(),
161
167
  })
162
- .passthrough();
168
+ .loose();
163
169
  const ApplicationInfo = zod_1.z
164
170
  .object({
165
171
  // Live-verified (11.0.83): only applicationVersion is returned.
166
172
  applicationVersion: zod_1.z.string().optional(),
167
173
  })
168
- .passthrough();
174
+ .loose();
169
175
  const Site = zod_1.z
170
176
  .object({
171
177
  id: zod_1.z.string(),
172
178
  internalReference: zod_1.z.string().optional(),
173
179
  name: zod_1.z.string().optional(),
174
180
  })
175
- .passthrough();
181
+ .loose();
176
182
  const Network = zod_1.z
177
183
  .object({
178
184
  id: zod_1.z.string(),
@@ -192,12 +198,12 @@ const Network = zod_1.z
192
198
  ipv6Configuration: zod_1.z.unknown().optional(),
193
199
  metadata: Metadata.optional(),
194
200
  })
195
- .passthrough();
201
+ .loose();
196
202
  const NetworkReferences = zod_1.z
197
203
  .object({
198
204
  referenceResources: zod_1.z.array(zod_1.z.unknown()).optional(),
199
205
  })
200
- .passthrough();
206
+ .loose();
201
207
  const Voucher = zod_1.z
202
208
  .object({
203
209
  id: zod_1.z.string(),
@@ -214,7 +220,7 @@ const Voucher = zod_1.z
214
220
  authorizedGuestCount: zod_1.z.number().optional(),
215
221
  expired: zod_1.z.boolean().optional(),
216
222
  })
217
- .passthrough();
223
+ .loose();
218
224
  const FirewallZone = zod_1.z
219
225
  .object({
220
226
  id: zod_1.z.string(),
@@ -222,7 +228,7 @@ const FirewallZone = zod_1.z
222
228
  networkIds: zod_1.z.array(zod_1.z.string()).optional(),
223
229
  metadata: Metadata.optional(),
224
230
  })
225
- .passthrough();
231
+ .loose();
226
232
  const FirewallPolicy = zod_1.z
227
233
  .object({
228
234
  id: zod_1.z.string(),
@@ -244,7 +250,7 @@ const FirewallPolicy = zod_1.z
244
250
  description: zod_1.z.string().optional(),
245
251
  metadata: Metadata.optional(),
246
252
  })
247
- .passthrough();
253
+ .loose();
248
254
  const FirewallPolicyOrdering = zod_1.z
249
255
  .object({
250
256
  orderedFirewallPolicyIds: zod_1.z
@@ -252,10 +258,10 @@ const FirewallPolicyOrdering = zod_1.z
252
258
  beforeSystemDefined: zod_1.z.array(zod_1.z.string()).optional(),
253
259
  afterSystemDefined: zod_1.z.array(zod_1.z.string()).optional(),
254
260
  })
255
- .passthrough()
261
+ .loose()
256
262
  .optional(),
257
263
  })
258
- .passthrough();
264
+ .loose();
259
265
  const AclRule = zod_1.z
260
266
  .object({
261
267
  id: zod_1.z.string(),
@@ -276,12 +282,12 @@ const AclRule = zod_1.z
276
282
  index: zod_1.z.number().int().optional(),
277
283
  metadata: Metadata.optional(),
278
284
  })
279
- .passthrough();
285
+ .loose();
280
286
  const AclRuleOrdering = zod_1.z
281
287
  .object({
282
288
  orderedAclRuleIds: zod_1.z.array(zod_1.z.string()).optional(),
283
289
  })
284
- .passthrough();
290
+ .loose();
285
291
  const SwitchStack = zod_1.z
286
292
  .object({
287
293
  id: zod_1.z.string(),
@@ -296,7 +302,7 @@ const SwitchStack = zod_1.z
296
302
  lags: zod_1.z.array(zod_1.z.unknown()).optional(),
297
303
  metadata: Metadata.optional(),
298
304
  })
299
- .passthrough();
305
+ .loose();
300
306
  const McLagDomain = zod_1.z
301
307
  .object({
302
308
  id: zod_1.z.string(),
@@ -305,7 +311,7 @@ const McLagDomain = zod_1.z
305
311
  lags: zod_1.z.array(zod_1.z.unknown()).optional(),
306
312
  metadata: Metadata.optional(),
307
313
  })
308
- .passthrough();
314
+ .loose();
309
315
  const Lag = zod_1.z
310
316
  .object({
311
317
  id: zod_1.z.string(),
@@ -316,11 +322,11 @@ const Lag = zod_1.z
316
322
  deviceId: zod_1.z.string().optional(),
317
323
  portIdxs: zod_1.z.array(zod_1.z.number()).optional(),
318
324
  })
319
- .passthrough())
325
+ .loose())
320
326
  .optional(),
321
327
  metadata: Metadata.optional(),
322
328
  })
323
- .passthrough();
329
+ .loose();
324
330
  const DnsPolicy = zod_1.z
325
331
  .object({
326
332
  id: zod_1.z.string(),
@@ -344,7 +350,7 @@ const DnsPolicy = zod_1.z
344
350
  ipAddress: zod_1.z.string().nullable().optional(),
345
351
  metadata: Metadata.optional(),
346
352
  })
347
- .passthrough();
353
+ .loose();
348
354
  const TrafficMatchingList = zod_1.z
349
355
  .object({
350
356
  id: zod_1.z.string(),
@@ -353,7 +359,7 @@ const TrafficMatchingList = zod_1.z
353
359
  items: zod_1.z.array(zod_1.z.unknown()).optional(),
354
360
  metadata: Metadata.optional(),
355
361
  })
356
- .passthrough();
362
+ .loose();
357
363
  // Verified against the live Integration API (11.0.83). The WAN and
358
364
  // site-to-site-tunnel list rows are intentionally minimal in the API.
359
365
  const Wan = zod_1.z
@@ -361,7 +367,7 @@ const Wan = zod_1.z
361
367
  id: zod_1.z.string(),
362
368
  name: zod_1.z.string().optional(),
363
369
  })
364
- .passthrough();
370
+ .loose();
365
371
  const VpnServer = zod_1.z
366
372
  .object({
367
373
  id: zod_1.z.string(),
@@ -370,21 +376,21 @@ const VpnServer = zod_1.z
370
376
  enabled: zod_1.z.boolean().optional(),
371
377
  metadata: Metadata.optional(),
372
378
  })
373
- .passthrough();
379
+ .loose();
374
380
  // No tunnels present in the verification environment — keep loose.
375
381
  const VpnTunnel = zod_1.z
376
382
  .object({
377
383
  id: zod_1.z.string(),
378
384
  name: zod_1.z.string().optional(),
379
385
  })
380
- .passthrough();
386
+ .loose();
381
387
  const RadiusProfile = zod_1.z
382
388
  .object({
383
389
  id: zod_1.z.string(),
384
390
  name: zod_1.z.string().optional(),
385
391
  metadata: Metadata.optional(),
386
392
  })
387
- .passthrough();
393
+ .loose();
388
394
  // No tags present in the verification environment — documented shape.
389
395
  const DeviceTag = zod_1.z
390
396
  .object({
@@ -392,27 +398,27 @@ const DeviceTag = zod_1.z
392
398
  name: zod_1.z.string().optional(),
393
399
  deviceIds: zod_1.z.array(zod_1.z.string()).optional(),
394
400
  })
395
- .passthrough();
401
+ .loose();
396
402
  // DPI category/application ids are NUMERIC (verified live).
397
403
  const DpiCategory = zod_1.z
398
404
  .object({
399
405
  id: zod_1.z.number(),
400
406
  name: zod_1.z.string().optional(),
401
407
  })
402
- .passthrough();
408
+ .loose();
403
409
  const DpiApplication = zod_1.z
404
410
  .object({
405
411
  id: zod_1.z.number(),
406
412
  name: zod_1.z.string().optional(),
407
413
  })
408
- .passthrough();
414
+ .loose();
409
415
  // Countries key on `code` (ISO alpha-2), NOT `id` (verified live).
410
416
  const Country = zod_1.z
411
417
  .object({
412
418
  code: zod_1.z.string(),
413
419
  name: zod_1.z.string().optional(),
414
420
  })
415
- .passthrough();
421
+ .loose();
416
422
  // No pending devices in the verification environment — keep loose.
417
423
  const PendingDevice = zod_1.z
418
424
  .object({
@@ -421,7 +427,31 @@ const PendingDevice = zod_1.z
421
427
  ipAddress: zod_1.z.string().optional(),
422
428
  firmwareVersion: zod_1.z.string().optional(),
423
429
  })
424
- .passthrough();
430
+ .loose();
431
+ // A write tool returns either the API's echo of the resource or, with dryRun,
432
+ // a preview of the request it would send. The SDK validates both against the
433
+ // one outputSchema, so the resource's key fields relax to optional alongside
434
+ // the preview fields; read tools keep their stricter schemas.
435
+ const DryRunPreview = {
436
+ dryRun: zod_1.z.literal(true).optional(),
437
+ wouldExecute: zod_1.z
438
+ .object({ method: zod_1.z.string(), path: zod_1.z.string(), body: zod_1.z.unknown().optional() })
439
+ .loose()
440
+ .optional(),
441
+ confirmationRequired: zod_1.z.string().optional(),
442
+ };
443
+ /**
444
+ * The outputSchema to publish for a tool: always a loose object, whether the
445
+ * tool passed a raw shape or a Zod object (the SDK accepts both). Wrapping a
446
+ * Zod object in z.looseObject() would build a broken schema that makes
447
+ * tools/list throw for every tool.
448
+ */
449
+ function looseOutput(schema) {
450
+ return schema instanceof zod_1.z.ZodObject ? schema.loose() : zod_1.z.looseObject(schema);
451
+ }
452
+ function writeResult(shape) {
453
+ return { ...zod_1.z.object(shape).partial().shape, ...DryRunPreview };
454
+ }
425
455
  exports.getInfoOutputSchema = ApplicationInfo.shape;
426
456
  exports.listSitesOutputSchema = {
427
457
  ...PaginationFields,
@@ -552,7 +582,7 @@ exports.HistoryClient = zod_1.z
552
582
  is_wired: zod_1.z.boolean().optional(),
553
583
  status: zod_1.z.string().optional(),
554
584
  })
555
- .passthrough();
585
+ .loose();
556
586
  exports.ClientSession = zod_1.z
557
587
  .object({
558
588
  _id: zod_1.z.string(),
@@ -565,15 +595,18 @@ exports.ClientSession = zod_1.z
565
595
  is_wired: zod_1.z.boolean().optional(),
566
596
  roaming_sessions: zod_1.z.array(zod_1.z.unknown()).optional(),
567
597
  })
568
- .passthrough();
598
+ .loose();
569
599
  /** Envelope returned by the controller's stat/session endpoint. */
570
600
  exports.ClientSessionEnvelope = zod_1.z.object({
571
- meta: zod_1.z.object({ rc: zod_1.z.literal("ok") }).passthrough(),
601
+ meta: zod_1.z.object({ rc: zod_1.z.literal("ok") }).loose(),
572
602
  data: zod_1.z.array(exports.ClientSession),
573
603
  });
574
604
  exports.listClientHistoryOutputSchema = {
575
605
  data: zod_1.z.array(exports.HistoryClient),
576
606
  count: zod_1.z.number(),
607
+ totalCount: zod_1.z.number(),
608
+ limit: zod_1.z.number(),
609
+ truncated: zod_1.z.boolean(),
577
610
  withinHours: zod_1.z.number(),
578
611
  coverage: zod_1.z.string(),
579
612
  };
@@ -586,3 +619,146 @@ exports.listClientSessionsOutputSchema = {
586
619
  end: zod_1.z.number(),
587
620
  coverage: zod_1.z.string(),
588
621
  };
622
+ // Classic controller API reads (rest/networkconf, rest/firewallgroup,
623
+ // rest/firewallrule, rest/portforward, get/setting). Unlike every other schema
624
+ // here, each row shape is also the tool's output allowlist: the tools copy only
625
+ // these fields from a record, so a field the controller adds never reaches the
626
+ // model. networkconf records carry WAN, VPN and RADIUS credentials
627
+ // (x_passphrase, x_wan_password, x_ipsec_*, x_openvpn_*, WireGuard keys, PPPoE
628
+ // logins), and settings sections carry more (x_ssh_password, x_mgmt_key,
629
+ // cloud-access keys); never list one here. Field types vary across controller
630
+ // versions, hence the unions.
631
+ const classicScalar = zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).nullish();
632
+ const classicString = zod_1.z.string().nullish();
633
+ const classicBoolean = zod_1.z.boolean().nullish();
634
+ exports.ClassicNetwork = zod_1.z
635
+ .object({
636
+ _id: zod_1.z.string(),
637
+ name: classicString,
638
+ purpose: classicString,
639
+ vlan: classicScalar,
640
+ vlan_enabled: classicBoolean,
641
+ ip_subnet: classicString,
642
+ networkgroup: classicString,
643
+ enabled: classicBoolean,
644
+ dhcpd_enabled: classicBoolean,
645
+ dhcpd_start: classicString,
646
+ dhcpd_stop: classicString,
647
+ domain_name: classicString,
648
+ igmp_snooping: classicBoolean,
649
+ mdns_enabled: classicBoolean,
650
+ network_isolation_enabled: classicBoolean,
651
+ internet_access_enabled: classicBoolean,
652
+ firewall_zone_id: classicString,
653
+ is_nat: classicBoolean,
654
+ ipv6_interface_type: classicString,
655
+ setting_preference: classicString,
656
+ })
657
+ .loose();
658
+ exports.ClassicFirewallGroup = zod_1.z
659
+ .object({
660
+ _id: zod_1.z.string(),
661
+ name: classicString,
662
+ group_type: classicString,
663
+ group_members: zod_1.z.array(zod_1.z.string()).nullish(),
664
+ })
665
+ .loose();
666
+ exports.LegacyFirewallRule = zod_1.z
667
+ .object({
668
+ _id: zod_1.z.string(),
669
+ name: classicString,
670
+ enabled: classicBoolean,
671
+ ruleset: classicString,
672
+ rule_index: classicScalar,
673
+ action: classicString,
674
+ protocol: classicString,
675
+ protocol_v6: classicString,
676
+ protocol_match_excepted: classicBoolean,
677
+ icmp_typename: classicString,
678
+ icmpv6_typename: classicString,
679
+ logging: classicBoolean,
680
+ state_established: classicBoolean,
681
+ state_related: classicBoolean,
682
+ state_new: classicBoolean,
683
+ state_invalid: classicBoolean,
684
+ src_firewallgroup_ids: zod_1.z.array(zod_1.z.string()).nullish(),
685
+ src_networkconf_id: classicString,
686
+ src_networkconf_type: classicString,
687
+ src_address: classicString,
688
+ src_address_ipv6: classicString,
689
+ src_mac_address: classicString,
690
+ src_port: classicScalar,
691
+ dst_firewallgroup_ids: zod_1.z.array(zod_1.z.string()).nullish(),
692
+ dst_networkconf_id: classicString,
693
+ dst_networkconf_type: classicString,
694
+ dst_address: classicString,
695
+ dst_address_ipv6: classicString,
696
+ dst_port: classicScalar,
697
+ ipsec: classicString,
698
+ setting_preference: classicString,
699
+ })
700
+ .loose();
701
+ /** Envelope of a classic REST collection; rows stay unknown until the allowlist is applied. */
702
+ exports.ClassicEnvelope = zod_1.z.object({
703
+ meta: zod_1.z.object({ rc: zod_1.z.literal("ok") }).loose(),
704
+ data: zod_1.z.array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown())),
705
+ });
706
+ const classicList = (row) => ({
707
+ data: zod_1.z.array(row),
708
+ count: zod_1.z.number(),
709
+ siteReference: zod_1.z.string(),
710
+ });
711
+ exports.listClassicNetworksOutputSchema = classicList(exports.ClassicNetwork);
712
+ exports.listFirewallGroupsOutputSchema = classicList(exports.ClassicFirewallGroup);
713
+ exports.listLegacyFirewallRulesOutputSchema = classicList(exports.LegacyFirewallRule);
714
+ exports.PortForwardDestination = zod_1.z
715
+ .object({
716
+ destination_ip: classicString,
717
+ interface: classicString,
718
+ })
719
+ .loose();
720
+ exports.PortForward = zod_1.z
721
+ .object({
722
+ _id: zod_1.z.string(),
723
+ name: classicString,
724
+ enabled: classicBoolean,
725
+ pfwd_interface: classicString,
726
+ destination_ip: classicString,
727
+ destination_ips: zod_1.z.array(exports.PortForwardDestination).nullish(),
728
+ src: classicString,
729
+ src_limiting_enabled: classicBoolean,
730
+ src_limiting_type: classicString,
731
+ src_firewall_group_id: classicString,
732
+ dst_port: classicScalar,
733
+ fwd: classicString,
734
+ fwd_port: classicScalar,
735
+ proto: classicString,
736
+ log: classicBoolean,
737
+ })
738
+ .loose();
739
+ exports.listPortForwardsOutputSchema = {
740
+ data: zod_1.z.array(exports.PortForward),
741
+ count: zod_1.z.number(),
742
+ enabledCount: zod_1.z.number(),
743
+ siteReference: zod_1.z.string(),
744
+ };
745
+ const settingsStatus = zod_1.z.enum(["available", "unavailable"]);
746
+ exports.getUpnpStatusOutputSchema = {
747
+ siteReference: zod_1.z.string(),
748
+ status: settingsStatus,
749
+ source: zod_1.z.string().nullable(),
750
+ upnp_enabled: zod_1.z.boolean().nullable(),
751
+ upnp_nat_pmp_enabled: zod_1.z.boolean().nullable(),
752
+ upnp_secure_mode: zod_1.z.boolean().nullable(),
753
+ upnp_wan_interface: zod_1.z.string().nullable(),
754
+ detail: zod_1.z.string(),
755
+ };
756
+ exports.getDeviceSshSettingsOutputSchema = {
757
+ siteReference: zod_1.z.string(),
758
+ status: settingsStatus,
759
+ x_ssh_enabled: zod_1.z.boolean().nullable(),
760
+ x_ssh_auth_password_enabled: zod_1.z.boolean().nullable(),
761
+ x_ssh_bind_wildcard: zod_1.z.boolean().nullable(),
762
+ sshKeyCount: zod_1.z.number().nullable(),
763
+ detail: zod_1.z.string(),
764
+ };
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * An Integration API resource ID. IDs are interpolated into request paths, so
4
+ * only the plain ID charset is accepted — anything else could smuggle a query
5
+ * (`?force=true`), a fragment, or extra path segments into the request.
6
+ */
7
+ export declare function resourceId(): z.ZodString;
8
+ /**
9
+ * A site's internalReference (often default), which the controller routes
10
+ * outside the Integration API take in place of the site UUID.
11
+ */
12
+ export declare function siteReference(): z.ZodString;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceId = resourceId;
4
+ exports.siteReference = siteReference;
5
+ const zod_1 = require("zod");
6
+ /**
7
+ * An Integration API resource ID. IDs are interpolated into request paths, so
8
+ * only the plain ID charset is accepted — anything else could smuggle a query
9
+ * (`?force=true`), a fragment, or extra path segments into the request.
10
+ */
11
+ function resourceId() {
12
+ return zod_1.z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "Use an ID returned by a list or get tool");
13
+ }
14
+ /**
15
+ * A site's internalReference (often default), which the controller routes
16
+ * outside the Integration API take in place of the site UUID.
17
+ */
18
+ function siteReference() {
19
+ return zod_1.z.string().regex(/^[A-Za-z0-9_-]{1,64}$/)
20
+ .describe("Site internalReference from unifi_list_sites (often default), not the Integration API UUID");
21
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copy only the allowlisted fields of a record. Classic records hold
3
+ * credentials under names no redaction list could anticipate, so the output is
4
+ * built up from the schema's keys and nothing is ever passed through.
5
+ */
6
+ export declare function pick(row: Record<string, unknown>, fields: readonly string[]): Record<string, unknown>;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pick = pick;
4
+ /**
5
+ * Copy only the allowlisted fields of a record. Classic records hold
6
+ * credentials under names no redaction list could anticipate, so the output is
7
+ * built up from the schema's keys and nothing is ever passed through.
8
+ */
9
+ function pick(row, fields) {
10
+ return Object.fromEntries(fields.filter((field) => Object.hasOwn(row, field)).map((field) => [field, row[field]]));
11
+ }
@@ -7,6 +7,8 @@ export declare function formatSuccess(data: unknown, opts?: {
7
7
  }[];
8
8
  structuredContent?: Record<string, unknown>;
9
9
  };
10
+ /** The message of a thrown value, which need not be an Error. */
11
+ export declare function errorMessage(err: unknown): string;
10
12
  export declare function formatError(err: unknown): {
11
13
  content: {
12
14
  type: "text";
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatSuccess = formatSuccess;
4
+ exports.errorMessage = errorMessage;
4
5
  exports.formatError = formatError;
5
6
  function formatSuccess(data, opts) {
6
7
  const result = {
7
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8
+ content: [{ type: "text", text: JSON.stringify(data) }],
8
9
  };
9
10
  if (opts?.structured &&
10
11
  data !== null &&
@@ -14,10 +15,35 @@ function formatSuccess(data, opts) {
14
15
  }
15
16
  return result;
16
17
  }
18
+ /** The message of a thrown value, which need not be an Error. */
19
+ function errorMessage(err) {
20
+ return err instanceof Error ? err.message : String(err);
21
+ }
17
22
  function formatError(err) {
18
- const message = err instanceof Error ? err.message : String(err);
23
+ let message = errorMessage(err);
24
+ // fetch reports every network failure as "fetch failed" and keeps the reason
25
+ // (ECONNREFUSED, ENOTFOUND, a certificate error) in cause.
26
+ const cause = err instanceof Error ? err.cause : undefined;
27
+ const code = cause instanceof Error && "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
28
+ if (code) {
29
+ message += ` (${code})`;
30
+ message += tlsHint(code);
31
+ }
19
32
  return {
20
33
  content: [{ type: "text", text: `Error: ${message}` }],
21
34
  isError: true,
22
35
  };
23
36
  }
37
+ /**
38
+ * What to do about a certificate that did not verify. Pinning makes a
39
+ * console's own certificate trusted, but it does not lift the validity dates,
40
+ * so an expired certificate gets its own advice.
41
+ */
42
+ function tlsHint(code) {
43
+ if (code === "CERT_HAS_EXPIRED" || code === "CERT_NOT_YET_VALID") {
44
+ return ". The console's TLS certificate is outside its validity dates; check the clocks on the console and this machine, and renew the certificate if it has expired (pinning it with UNIFI_NETWORK_CA_CERT does not override the dates)";
45
+ }
46
+ if (!/CERT|SELF_SIGNED|LEAF_SIGNATURE/.test(code))
47
+ return "";
48
+ return ". The console's TLS certificate did not verify. For a local console with a self-signed certificate, save the certificate and point UNIFI_NETWORK_CA_CERT at it; if it is already set, the console has a new certificate, so save it again. Against api.ui.com, something between you and Ubiquiti presented this certificate: do not work around it";
49
+ }