@highstate/library 0.9.3 → 0.9.5

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/src/wireguard.ts CHANGED
@@ -1,53 +1,23 @@
1
- import { defineEntity, defineUnit, Type, type Static } from "@highstate/contract"
2
- import {
3
- clusterEntity,
4
- deploymentEntity,
5
- interfaceEntity,
6
- serviceEntity,
7
- serviceTypeSchema,
8
- statefulSetEntity,
9
- } from "./k8s"
10
- import { providerEntity } from "./dns"
11
- import { l4EndpointEntity } from "./common"
1
+ import { defineEntity, defineUnit, Type, type Static, type TObject } from "@highstate/contract"
2
+ import { omit } from "remeda"
3
+ import { clusterEntity, interfaceEntity, exposableWorkloadEntity } from "./k8s"
4
+ import { l3EndpointEntity, l4EndpointEntity } from "./network"
5
+ import { arrayPatchModeSchema } from "./utils"
12
6
 
13
7
  export const backendSchema = Type.StringEnum(["wireguard", "amneziawg"])
14
- export const presharedKeyModeSchema = Type.StringEnum(["none", "global", "secure"])
15
8
 
16
9
  export type Backend = Static<typeof backendSchema>
17
- export type PresharedKeyMode = Static<typeof presharedKeyModeSchema>
18
10
 
19
11
  export const networkEntity = defineEntity({
20
12
  type: "wireguard.network",
21
13
 
22
14
  schema: Type.Object({
23
- backend: Type.Optional(backendSchema),
24
- presharedKeyMode: presharedKeyModeSchema,
25
- globalPresharedKey: Type.Optional(Type.String()),
26
- ipv6: Type.Optional(Type.Boolean()),
15
+ backend: backendSchema,
16
+ ipv6: Type.Boolean(),
27
17
  }),
28
18
  })
29
19
 
30
- export const identityEntity = defineEntity({
31
- type: "wireguard.identity",
32
-
33
- schema: Type.Object({
34
- name: Type.String(),
35
- network: Type.Optional(networkEntity.schema),
36
- address: Type.Optional(Type.String()),
37
- privateKey: Type.String(),
38
- presharedKeyPart: Type.Optional(Type.String()),
39
- k8sServices: Type.Array(serviceEntity.schema),
40
- exitNode: Type.Boolean(),
41
- listenPort: Type.Optional(Type.Number()),
42
- externalIp: Type.Optional(Type.String()),
43
- endpoint: Type.Optional(Type.String()),
44
- fqdn: Type.Optional(Type.String()),
45
- }),
46
-
47
- meta: {
48
- color: "#F44336",
49
- },
50
- })
20
+ export const nodeExposePolicySchema = Type.StringEnum(["always", "when-has-endpoint", "never"])
51
21
 
52
22
  export const peerEntity = defineEntity({
53
23
  type: "wireguard.peer",
@@ -58,10 +28,28 @@ export const peerEntity = defineEntity({
58
28
  publicKey: Type.String(),
59
29
  address: Type.Optional(Type.String()),
60
30
  allowedIps: Type.Array(Type.String()),
61
- endpoint: Type.Optional(Type.String()),
31
+ endpoints: Type.Array(l4EndpointEntity.schema),
32
+ allowedEndpoints: Type.Array(Type.Union([l3EndpointEntity.schema, l4EndpointEntity.schema])),
33
+
34
+ /**
35
+ * The pre-shared key of the WireGuard peer.
36
+ *
37
+ * If one of two peers has `presharedKey` set, the other peer must have `presharedKey` set too and they must be equal.
38
+ *
39
+ * Will be ignored if both peers have `presharedKeyPart` set.
40
+ */
41
+ presharedKey: Type.Optional(Type.String()),
42
+
43
+ /**
44
+ * The pre-shared key part of the WireGuard peer.
45
+ *
46
+ * If both peers have `presharedKeyPart` set, their `presharedKey` will be calculated as XOR of the two parts.
47
+ */
62
48
  presharedKeyPart: Type.Optional(Type.String()),
63
- excludedIps: Type.Optional(Type.Array(Type.String())),
64
- dns: Type.Optional(Type.Array(Type.String())),
49
+
50
+ excludedIps: Type.Array(Type.String()),
51
+ dns: Type.Array(Type.String()),
52
+ listenPort: Type.Optional(Type.Number()),
65
53
  }),
66
54
 
67
55
  meta: {
@@ -69,20 +57,23 @@ export const peerEntity = defineEntity({
69
57
  },
70
58
  })
71
59
 
72
- export const k8sNodeEntity = defineEntity({
73
- type: "wireguard.node",
60
+ export const identityEntity = defineEntity({
61
+ type: "wireguard.identity",
74
62
 
75
63
  schema: Type.Object({
76
- network: Type.String(),
77
- address: Type.String(),
78
- endpoint: Type.Optional(Type.String()),
79
- peers: Type.Array(Type.String()),
64
+ peer: peerEntity.schema,
65
+ privateKey: Type.String(),
80
66
  }),
67
+
68
+ meta: {
69
+ color: "#F44336",
70
+ },
81
71
  })
82
72
 
83
73
  export type Network = Static<typeof networkEntity.schema>
84
74
  export type Identity = Static<typeof identityEntity.schema>
85
75
  export type Peer = Static<typeof peerEntity.schema>
76
+ export type NodeExposePolicy = Static<typeof nodeExposePolicySchema>
86
77
 
87
78
  /**
88
79
  * The network hols the shared configuration for the WireGuard identities, peers and nodes.
@@ -104,23 +95,6 @@ export const network = defineUnit({
104
95
  */
105
96
  backend: Type.Default(backendSchema, "wireguard"),
106
97
 
107
- /**
108
- * The option which defines how to handle pre-shared keys between peers.
109
- *
110
- * 1. `none` - No pre-shared keys will be used.
111
- * 2. `global` - A single pre-shared key will be used for all peer pairs in the network.
112
- * 3. `secure` - Each peer pair will have its own pre-shared key.
113
- * In this case, each identity generates `presharedKeyPart` and the actual pre-shared key
114
- * for each peer pair will be computed as `xor(peer1.presharedKeyPart, peer2.presharedKeyPart)`.
115
- *
116
- * If the whole network is managed by the HighState, the `secure` mode is recommended.
117
- *
118
- * By default, the `none` mode is used.
119
- *
120
- * @schema
121
- */
122
- presharedKeyMode: Type.Optional(presharedKeyModeSchema),
123
-
124
98
  /**
125
99
  * The option to enable IPv6 support in the network.
126
100
  *
@@ -128,19 +102,7 @@ export const network = defineUnit({
128
102
  *
129
103
  * @schema
130
104
  */
131
- ipv6: Type.Optional(Type.Boolean()),
132
- },
133
-
134
- secrets: {
135
- /**
136
- * The global pre-shared key to use for all peer pairs in the network.
137
- *
138
- * Will be used only if `presharedKeyMode` is set to `global`.
139
- * Will be generated automatically if not provided.
140
- *
141
- * @schema
142
- */
143
- globalPresharedKey: Type.Optional(Type.String()),
105
+ ipv6: Type.Default(Type.Boolean(), false),
144
106
  },
145
107
 
146
108
  outputs: {
@@ -152,6 +114,7 @@ export const network = defineUnit({
152
114
  primaryIcon: "simple-icons:wireguard",
153
115
  primaryIconColor: "#88171a",
154
116
  secondaryIcon: "mdi:local-area-network-connect",
117
+ category: "VPN",
155
118
  },
156
119
 
157
120
  source: {
@@ -179,13 +142,6 @@ const sharedPeerArgs = {
179
142
  */
180
143
  address: Type.Optional(Type.String()),
181
144
 
182
- /**
183
- * The list of allowed IPs for the peer.
184
- *
185
- * @schema
186
- */
187
- allowedIps: Type.Optional(Type.Array(Type.String())),
188
-
189
145
  /**
190
146
  * The convenience option to set `allowedIps` to `0.0.0.0/0, ::/0`.
191
147
  *
@@ -193,7 +149,7 @@ const sharedPeerArgs = {
193
149
  *
194
150
  * @schema
195
151
  */
196
- exitNode: Type.Optional(Type.Boolean()),
152
+ exitNode: Type.Default(Type.Boolean(), false),
197
153
 
198
154
  /**
199
155
  * The list of IP ranges to exclude from the tunnel.
@@ -206,7 +162,7 @@ const sharedPeerArgs = {
206
162
  *
207
163
  * @schema
208
164
  */
209
- excludedIps: Type.Optional(Type.Array(Type.String())),
165
+ excludedIps: Type.Default(Type.Array(Type.String()), []),
210
166
 
211
167
  /**
212
168
  * The convenience option to exclude private IPs from the tunnel.
@@ -226,14 +182,23 @@ const sharedPeerArgs = {
226
182
  *
227
183
  * @schema
228
184
  */
229
- excludePrivateIps: Type.Optional(Type.Boolean()),
185
+ excludePrivateIps: Type.Default(Type.Boolean(), false),
230
186
 
231
187
  /**
232
- * The endpoint of the WireGuard peer.
188
+ * The endpoints of the WireGuard peer.
233
189
  *
234
190
  * @schema
235
191
  */
236
- endpoint: Type.Optional(Type.String()),
192
+ endpoints: Type.Default(Type.Array(Type.String()), []),
193
+
194
+ /**
195
+ * The allowed endpoints of the WireGuard peer.
196
+ *
197
+ * The non `hostname` endpoints will be added to the `allowedIps` of the peer.
198
+ *
199
+ * @schema
200
+ */
201
+ allowedEndpoints: Type.Default(Type.Array(Type.String()), []),
237
202
 
238
203
  /**
239
204
  * The DNS servers that should be used by the interface connected to the WireGuard peer.
@@ -242,7 +207,7 @@ const sharedPeerArgs = {
242
207
  *
243
208
  * @schema
244
209
  */
245
- dns: Type.Optional(Type.Array(Type.String())),
210
+ dns: Type.Default(Type.Array(Type.String()), []),
246
211
 
247
212
  /**
248
213
  * The convenience option to include the DNS servers to the allowed IPs.
@@ -251,28 +216,97 @@ const sharedPeerArgs = {
251
216
  *
252
217
  * @schema
253
218
  */
254
- includeDns: Type.Optional(Type.Boolean({ default: true })),
255
- }
219
+ includeDns: Type.Default(Type.Boolean(), true),
256
220
 
257
- const sharedInterfaceArgs = {
258
221
  /**
259
222
  * The port to listen on.
260
223
  *
261
- * Will override the `listenPort` of the identity if provided.
262
- *
263
224
  * @schema
264
225
  */
265
226
  listenPort: Type.Optional(Type.Number()),
227
+ }
266
228
 
229
+ const sharedPeerInputs = {
267
230
  /**
268
- * The DNS servers that should be used by the interface connected to the WireGuard node.
231
+ * The network to use for the WireGuard identity.
269
232
  *
270
- * Will be merged with the DNS servers of the peers.
233
+ * If not provided, the identity will use default network configuration.
271
234
  *
272
235
  * @schema
273
236
  */
274
- dns: Type.Optional(Type.Array(Type.String())),
275
- }
237
+ network: {
238
+ entity: networkEntity,
239
+ required: false,
240
+ },
241
+
242
+ /**
243
+ * The L3 endpoints of the identity.
244
+ *
245
+ * Will produce L4 endpoints for each of the provided L3 endpoints.
246
+ *
247
+ * @schema
248
+ */
249
+ l3Endpoints: {
250
+ entity: l3EndpointEntity,
251
+ multiple: true,
252
+ required: false,
253
+ },
254
+
255
+ /**
256
+ * The L4 endpoints of the identity.
257
+ *
258
+ * Will take priority over all calculated endpoints if provided.
259
+ *
260
+ * @schema
261
+ */
262
+ l4Endpoints: {
263
+ entity: l4EndpointEntity,
264
+ required: false,
265
+ multiple: true,
266
+ },
267
+
268
+ /**
269
+ * The L3 endpoints to add to the allowed IPs of the identity.
270
+ *
271
+ * `hostname` endpoints will be ignored.
272
+ *
273
+ * If the endpoint contains k8s service metadata of the cluster where the identity node is deployed,
274
+ * the corresponding network policy will be created.
275
+ *
276
+ * @schema
277
+ */
278
+ allowedL3Endpoints: {
279
+ entity: l3EndpointEntity,
280
+ multiple: true,
281
+ required: false,
282
+ },
283
+
284
+ /**
285
+ * The L4 endpoints to add to the allowed IPs of the identity.
286
+ *
287
+ * If the endpoint contains k8s service metadata of the cluster where the identity node is deployed,
288
+ * the corresponding network policy will be created.
289
+ *
290
+ * @schema
291
+ */
292
+ allowedL4Endpoints: {
293
+ entity: l4EndpointEntity,
294
+ multiple: true,
295
+ required: false,
296
+ },
297
+ } as const
298
+
299
+ const sharedPeerOutputs = {
300
+ peer: peerEntity,
301
+
302
+ endpoints: {
303
+ entity: l4EndpointEntity,
304
+ required: false,
305
+ multiple: true,
306
+ },
307
+ } as const
308
+
309
+ export type SharedPeerArgs = Static<TObject<typeof sharedPeerArgs>>
276
310
 
277
311
  export const peer = defineUnit({
278
312
  type: "wireguard.peer",
@@ -285,59 +319,101 @@ export const peer = defineUnit({
285
319
  *
286
320
  * @schema
287
321
  */
288
- publicKey: Type.Optional(Type.String()),
322
+ publicKey: Type.String(),
289
323
  },
290
324
 
291
- inputs: {
325
+ secrets: {
292
326
  /**
293
- * The network to use for the WireGuard peer.
294
- *
295
- * If not provided, the peer will use default network configuration.
327
+ * The pre-shared key which should be used for the peer.
296
328
  *
297
329
  * @schema
298
330
  */
299
- network: {
300
- entity: networkEntity,
301
- required: false,
302
- },
331
+ presharedKey: Type.Optional(Type.String()),
332
+ },
303
333
 
334
+ inputs: sharedPeerInputs,
335
+ outputs: sharedPeerOutputs,
336
+
337
+ meta: {
338
+ description: "The WireGuard peer with the public key.",
339
+ primaryIcon: "simple-icons:wireguard",
340
+ primaryIconColor: "#88171a",
341
+ secondaryIcon: "mdi:badge-account-horizontal",
342
+ category: "VPN",
343
+ },
344
+
345
+ source: {
346
+ package: "@highstate/wireguard",
347
+ path: "peer",
348
+ },
349
+ })
350
+
351
+ export const peerPatch = defineUnit({
352
+ type: "wireguard.peer-patch",
353
+
354
+ args: {
304
355
  /**
305
- * The existing WireGuard peer to extend.
356
+ * The endpoints of the WireGuard peer.
306
357
  *
307
358
  * @schema
308
359
  */
309
- peer: {
310
- entity: peerEntity,
311
- required: false,
312
- },
360
+ endpoints: Type.Default(Type.Array(Type.String()), []),
313
361
 
314
362
  /**
315
- * The L4 endpoint of the peer.
363
+ * The mode to use for patching the endpoints.
316
364
  *
317
- * Will take priority over all calculated endpoints if provided.
365
+ * - `prepend`: prepend the new endpoints to the existing ones (default);
366
+ * - `replace`: replace the existing endpoints with the new ones.
367
+ */
368
+ endpointsPatchMode: Type.Default(arrayPatchModeSchema, "prepend"),
369
+
370
+ /**
371
+ * The allowed endpoints of the WireGuard peer.
372
+ *
373
+ * The non `hostname` endpoints will be added to the `allowedIps` of the peer.
318
374
  *
319
375
  * @schema
320
376
  */
321
- l4Endpoint: {
322
- entity: l4EndpointEntity,
323
- required: false,
324
- },
377
+ allowedEndpoints: Type.Default(Type.Array(Type.String()), []),
378
+
379
+ /**
380
+ * The mode to use for patching the allowed endpoints.
381
+ *
382
+ * - `prepend`: prepend the new endpoints to the existing ones (default);
383
+ * - `replace`: replace the existing endpoints with the new ones.
384
+ */
385
+ allowedEndpointsPatchMode: Type.Default(arrayPatchModeSchema, "prepend"),
386
+
387
+ ...omit(sharedPeerArgs, ["endpoints", "allowedEndpoints"]),
388
+ },
389
+
390
+ inputs: {
391
+ peer: peerEntity,
392
+ ...sharedPeerInputs,
325
393
  },
326
394
 
327
395
  outputs: {
328
396
  peer: peerEntity,
397
+
398
+ endpoints: {
399
+ entity: l4EndpointEntity,
400
+ required: false,
401
+ multiple: true,
402
+ },
329
403
  },
330
404
 
331
405
  meta: {
332
- description: "The WireGuard peer with the public key.",
406
+ displayName: "WireGuard Peer Patch",
407
+ description: "Patches some properties of the WireGuard peer.",
333
408
  primaryIcon: "simple-icons:wireguard",
334
409
  primaryIconColor: "#88171a",
335
410
  secondaryIcon: "mdi:badge-account-horizontal",
411
+ category: "VPN",
336
412
  },
337
413
 
338
414
  source: {
339
415
  package: "@highstate/wireguard",
340
- path: "peer",
416
+ path: "peer-patch",
341
417
  },
342
418
  })
343
419
 
@@ -356,47 +432,16 @@ export const identity = defineUnit({
356
432
  */
357
433
  listenPort: Type.Optional(Type.Number()),
358
434
 
359
- /**
360
- * The external IP address of the WireGuard identity.
361
- *
362
- * Used by the implementation of the identity and to calculate the endpoint of the peer.
363
- *
364
- * @schema
365
- */
366
- externalIp: Type.Optional(Type.String()),
367
-
368
435
  /**
369
436
  * The endpoint of the WireGuard peer.
370
437
  *
371
- * By default, the endpoint is calculated as `externalIp:listenPort`.
372
- *
373
438
  * If overridden, does not affect node which implements the identity, but is used in the peer configuration of other nodes.
374
439
  *
375
440
  * Will take priority over all calculated endpoints and `l4Endpoint` input.
376
441
  *
377
442
  * @schema
378
443
  */
379
- endpoint: Type.Optional(Type.String()),
380
-
381
- /**
382
- * The FQDN of the WireGuard identity.
383
- * Will be used as endpoint for the peer.
384
- *
385
- * If `dnsProvider` is provided, external IP is available and `registerFqdn` is set to `true`, and FQDN is provided explicitly (not obtained from the k8s cluster),
386
- * the FQDN will be registered with the DNS provider.
387
- *
388
- * @schema
389
- */
390
- fqdn: Type.Optional(Type.String()),
391
-
392
- /**
393
- * Whether to register the FQDN of the identity with the matching DNS providers.
394
- *
395
- * By default, `true`.
396
- *
397
- * @schema
398
- */
399
- registerFqdn: Type.Default(Type.Boolean(), true),
444
+ endpoints: Type.Default(Type.Array(Type.String()), []),
400
445
  },
401
446
 
402
447
  secrets: {
@@ -419,76 +464,11 @@ export const identity = defineUnit({
419
464
  presharedKeyPart: Type.Optional(Type.String()),
420
465
  },
421
466
 
422
- inputs: {
423
- /**
424
- * The network to use for the WireGuard identity.
425
- *
426
- * If not provided, the identity will use default network configuration.
427
- *
428
- * @schema
429
- */
430
- network: {
431
- entity: networkEntity,
432
- required: false,
433
- },
434
-
435
- /**
436
- * The list of Kubernetes services to expose the WireGuard identity.
437
- *
438
- * Their IP addresses will be added to the `allowedIps` of the identity and passed to the node to set up network policies.
439
- *
440
- * @schema
441
- */
442
- k8sServices: {
443
- entity: serviceEntity,
444
- multiple: true,
445
- required: false,
446
- },
447
-
448
- /**
449
- * The Kubernetes cluster associated with the identity.
450
- *
451
- * If provided, will be used to obtain the external IP or FQDN of the identity.
452
- *
453
- * @schema
454
- */
455
- k8sCluster: {
456
- entity: clusterEntity,
457
- required: false,
458
- },
459
-
460
- /**
461
- * The L4 endpoint of the identity.
462
- *
463
- * Will take priority over all calculated endpoints if provided.
464
- *
465
- * @schema
466
- */
467
- l4Endpoint: {
468
- entity: l4EndpointEntity,
469
- required: false,
470
- },
471
-
472
- /**
473
- * The DNS providers to register the FQDN of the identity with.
474
- *
475
- * @schema
476
- */
477
- dnsProviders: {
478
- entity: providerEntity,
479
- required: false,
480
- multiple: true,
481
- },
482
- },
467
+ inputs: sharedPeerInputs,
483
468
 
484
469
  outputs: {
485
470
  identity: identityEntity,
486
- peer: peerEntity,
487
-
488
- l4Endpoint: {
489
- entity: l4EndpointEntity,
490
- required: false,
491
- },
471
+ ...sharedPeerOutputs,
492
472
  },
493
473
 
494
474
  meta: {
@@ -496,6 +476,7 @@ export const identity = defineUnit({
496
476
  primaryIcon: "simple-icons:wireguard",
497
477
  primaryIconColor: "#88171a",
498
478
  secondaryIcon: "mdi:account",
479
+ category: "VPN",
499
480
  },
500
481
 
501
482
  source: {
@@ -508,19 +489,32 @@ export const node = defineUnit({
508
489
  type: "wireguard.node",
509
490
 
510
491
  args: {
492
+ /**
493
+ * The name of the namespace/deployment/statefulset where the WireGuard node will be deployed.
494
+ *
495
+ * By default, the name is `wg-${identity.name}`.
496
+ *
497
+ * @schema
498
+ */
511
499
  appName: Type.Optional(Type.String()),
512
- serviceType: Type.Optional(serviceTypeSchema),
513
500
 
514
- ...sharedInterfaceArgs,
501
+ /**
502
+ * Whether to expose the WireGuard node to the outside world.
503
+ *
504
+ * @schema
505
+ */
506
+ external: Type.Default(Type.Boolean(), false),
515
507
 
516
508
  /**
517
- * The external IP address of the WireGuard node.
509
+ * The policy to use for exposing the WireGuard node.
518
510
  *
519
- * Will override the `externalIp` of the identity if provided.
511
+ * - `always` - The node will be exposed and the service will be created.
512
+ * - `when-has-endpoint` - The node will be exposed only if the provided idenity has at least one endpoint.
513
+ * - `never` - The node will not be exposed and the service will not be created.
520
514
  *
521
- * @schema
515
+ * * By default, the `when-has-endpoint` policy is used.
522
516
  */
523
- externalIp: Type.Optional(Type.String()),
517
+ exposePolicy: Type.Default(nodeExposePolicySchema, "when-has-endpoint"),
524
518
 
525
519
  /**
526
520
  * The extra specification of the container which runs the WireGuard node.
@@ -536,13 +530,8 @@ export const node = defineUnit({
536
530
  identity: identityEntity,
537
531
  k8sCluster: clusterEntity,
538
532
 
539
- deployment: {
540
- entity: deploymentEntity,
541
- required: false,
542
- },
543
-
544
- statefulSet: {
545
- entity: statefulSetEntity,
533
+ workload: {
534
+ entity: exposableWorkloadEntity,
546
535
  required: false,
547
536
  },
548
537
 
@@ -559,19 +548,20 @@ export const node = defineUnit({
559
548
  },
560
549
 
561
550
  outputs: {
562
- deployment: {
563
- entity: deploymentEntity,
551
+ interface: {
552
+ entity: interfaceEntity,
564
553
  required: false,
565
554
  },
566
555
 
567
- interface: {
568
- entity: interfaceEntity,
556
+ peer: {
557
+ entity: peerEntity,
569
558
  required: false,
570
559
  },
571
560
 
572
- service: {
573
- entity: serviceEntity,
561
+ endpoints: {
562
+ entity: l4EndpointEntity,
574
563
  required: false,
564
+ multiple: true,
575
565
  },
576
566
  },
577
567
 
@@ -580,6 +570,7 @@ export const node = defineUnit({
580
570
  primaryIcon: "simple-icons:wireguard",
581
571
  primaryIconColor: "#88171a",
582
572
  secondaryIcon: "mdi:server",
573
+ category: "VPN",
583
574
  },
584
575
 
585
576
  source: {
@@ -592,8 +583,6 @@ export const config = defineUnit({
592
583
  type: "wireguard.config",
593
584
 
594
585
  args: {
595
- ...sharedInterfaceArgs,
596
-
597
586
  /**
598
587
  * The name of the "default" interface where non-tunneled traffic should go.
599
588
  *
@@ -619,6 +608,7 @@ export const config = defineUnit({
619
608
  primaryIcon: "simple-icons:wireguard",
620
609
  primaryIconColor: "#88171a",
621
610
  secondaryIcon: "mdi:settings",
611
+ category: "VPN",
622
612
  },
623
613
 
624
614
  source: {
@@ -649,6 +639,7 @@ export const configBundle = defineUnit({
649
639
  primaryIcon: "simple-icons:wireguard",
650
640
  primaryIconColor: "#88171a",
651
641
  secondaryIcon: "mdi:folder-settings-variant",
642
+ category: "VPN",
652
643
  },
653
644
 
654
645
  source: {