@highstate/library 0.9.4 → 0.9.6

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/k8s.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { defineEntity, defineUnit, Type, type Static } from "@highstate/contract"
2
2
  import { Literal } from "@sinclair/typebox"
3
- import { providerEntity } from "./dns"
3
+ import * as dns from "./dns"
4
+ import { l3EndpointEntity, l4EndpointEntity } from "./network"
5
+ import { serverEntity } from "./common"
6
+ import { arrayPatchModeSchema } from "./utils"
7
+
8
+ export const fallbackKubeApiAccessSchema = Type.Object({
9
+ serverIp: Type.String(),
10
+ serverPort: Type.Number(),
11
+ })
4
12
 
5
13
  export const tunDevicePolicySchema = Type.Union([
6
14
  Type.Object({
@@ -13,14 +21,19 @@ export const tunDevicePolicySchema = Type.Union([
13
21
  }),
14
22
  ])
15
23
 
16
- export const clusterInfoSchema = Type.Object({
17
- id: Type.String(),
18
- name: Type.String(),
19
- cni: Type.Optional(Type.String()),
20
- externalIps: Type.Array(Type.String()),
21
- fqdn: Type.Optional(Type.String()),
22
- kubeApiServerIp: Type.Optional(Type.String()),
23
- kubeApiServerPort: Type.Optional(Type.Number()),
24
+ export const externalServiceTypeSchema = Type.StringEnum(["NodePort", "LoadBalancer"])
25
+ export const scheduleOnMastersPolicySchema = Type.StringEnum(["always", "when-no-workers", "never"])
26
+ export const cniSchema = Type.StringEnum(["cilium", "other"])
27
+
28
+ export const clusterQuirksSchema = Type.Object({
29
+ /**
30
+ * The IP and port of the kube-apiserver available from the cluster.
31
+ *
32
+ * Will be used to create fallback network policy in CNIs which does not support allowing access to the kube-apiserver.
33
+ *
34
+ * @schema
35
+ */
36
+ fallbackKubeApiAccess: Type.Optional(fallbackKubeApiAccessSchema),
24
37
 
25
38
  /**
26
39
  * Specifies the policy for using the tun device inside containers.
@@ -28,33 +41,96 @@ export const clusterInfoSchema = Type.Object({
28
41
  * If not provided, the default policy is `host` which assumes just mounting /dev/net/tun from the host.
29
42
  *
30
43
  * For some runtimes, like Talos's one, the /dev/net/tun device is not available in the host, so the plugin policy should be used.
44
+ *
45
+ * @schema
31
46
  */
32
47
  tunDevicePolicy: Type.Optional(tunDevicePolicySchema),
48
+
49
+ /**
50
+ * The service type to use for external services.
51
+ *
52
+ * If not provided, the default service type is `NodePort` since `LoadBalancer` may not be available.
53
+ *
54
+ * @schema
55
+ */
56
+ externalServiceType: Type.Optional(externalServiceTypeSchema),
33
57
  })
34
58
 
59
+ export const clusterInfoProperties = {
60
+ /**
61
+ * The unique identifier of the cluster.
62
+ *
63
+ * Should be defined as a UUID of the `kube-system` namespace which is always present in the cluster.
64
+ *
65
+ * @schema
66
+ */
67
+ id: Type.String(),
68
+
69
+ /**
70
+ * The name of the cluster.
71
+ *
72
+ * @schema
73
+ */
74
+ name: Type.String(),
75
+
76
+ /**
77
+ * The name of the CNI plugin used by the cluster.
78
+ *
79
+ * Supported values are:
80
+ * - `cilium`
81
+ * - `other`
82
+ *
83
+ * @schema
84
+ */
85
+ cni: cniSchema,
86
+
87
+ /**
88
+ * The endpoints of the cluster nodes.
89
+ *
90
+ * The entry may represent real node endpoint or virtual endpoint (like a load balancer).
91
+ *
92
+ * The same node may also be represented by multiple entries (e.g. a node with private and public IP).
93
+ *
94
+ * @schema
95
+ */
96
+ endpoints: Type.Array(l3EndpointEntity.schema),
97
+
98
+ /**
99
+ * The endpoints of the API server.
100
+ *
101
+ * The entry may represent real node endpoint or virtual endpoint (like a load balancer).
102
+ *
103
+ * The same node may also be represented by multiple entries (e.g. a node with private and public IP).
104
+ */
105
+ apiEndpoints: Type.Array(l4EndpointEntity.schema),
106
+
107
+ /**
108
+ * The external IPs of the cluster nodes allowed to be used for external access.
109
+ *
110
+ * @schema
111
+ */
112
+ externalIps: Type.Array(Type.String()),
113
+
114
+ /**
115
+ * The extra quirks of the cluster to improve compatibility.
116
+ *
117
+ * @schema
118
+ */
119
+ quirks: Type.Optional(clusterQuirksSchema),
120
+ } as const
121
+
35
122
  export const serviceTypeSchema = Type.StringEnum(["NodePort", "LoadBalancer", "ClusterIP"])
36
123
 
37
124
  export const metadataSchema = Type.Object({
38
- namespace: Type.Optional(Type.String()),
39
125
  name: Type.String(),
126
+ namespace: Type.String(),
40
127
  labels: Type.Optional(Type.Record(Type.String(), Type.String())),
41
128
  annotations: Type.Optional(Type.Record(Type.String(), Type.String())),
42
129
  })
43
130
 
44
- export const servicePortSchema = Type.Object({
45
- name: Type.Optional(Type.String()),
46
- port: Type.Optional(Type.Number()),
47
- targetPort: Type.Optional(Type.Union([Type.Number(), Type.String()])),
48
- protocol: Type.Optional(Type.String()),
49
- })
50
-
51
- export const serviceSpecSchema = Type.Object({
52
- type: Type.Optional(Type.String()),
53
- selector: Type.Record(Type.String(), Type.String()),
54
- ports: Type.Array(servicePortSchema),
55
- clusterIP: Type.Optional(Type.String()),
56
- clusterIPs: Type.Optional(Type.Array(Type.String())),
57
- externalIPs: Type.Optional(Type.Array(Type.String())),
131
+ export const resourceSchema = Type.Object({
132
+ clusterId: Type.String(),
133
+ metadata: metadataSchema,
58
134
  })
59
135
 
60
136
  export const serviceEntity = defineEntity({
@@ -62,9 +138,8 @@ export const serviceEntity = defineEntity({
62
138
 
63
139
  schema: Type.Object({
64
140
  type: Type.Literal("k8s.service"),
65
- clusterInfo: clusterInfoSchema,
66
- metadata: metadataSchema,
67
- spec: serviceSpecSchema,
141
+ ...resourceSchema.properties,
142
+ endpoints: Type.Array(l4EndpointEntity.schema),
68
143
  }),
69
144
 
70
145
  meta: {
@@ -76,7 +151,7 @@ export const clusterEntity = defineEntity({
76
151
  type: "k8s.cluster",
77
152
 
78
153
  schema: Type.Object({
79
- info: clusterInfoSchema,
154
+ ...clusterInfoProperties,
80
155
  kubeconfig: Type.String(),
81
156
  }),
82
157
 
@@ -87,62 +162,73 @@ export const clusterEntity = defineEntity({
87
162
 
88
163
  export const internalIpsPolicySchema = Type.StringEnum(["always", "public", "never"])
89
164
 
90
- export const sharedClusterArgs = {
165
+ export const scheduleOnMastersPolicyArgs = {
91
166
  /**
92
- * The list of external IPs of the cluster nodes allowed to be used for external access.
167
+ * The policy for scheduling workloads on master nodes.
93
168
  *
94
- * If not provided, will be automatically detected by querying the cluster nodes.
169
+ * - `always`: always schedule workloads on master nodes regardless of the number of workers;
170
+ * - `when-no-workers`: schedule workloads on master nodes only if there are no workers (default);
171
+ * - `never`: never schedule workloads on master nodes.
95
172
  *
96
173
  * @schema
97
174
  */
98
- externalIps: Type.Optional(Type.Array(Type.String())),
99
-
100
- /**
101
- * The policy for using internal IPs of the nodes as external IPs.
102
- *
103
- * - `always`: always use internal IPs as external IPs;
104
- * - `public`: use internal IPs as external IPs only if they are (theoretically) routable from the public internet **(default)**;
105
- * - `never`: never use internal IPs as external IPs.
106
- *
107
- * @schema
108
- */
109
- internalIpsPolicy: { ...internalIpsPolicySchema, default: "public" },
175
+ scheduleOnMastersPolicy: Type.Default(scheduleOnMastersPolicySchema, "when-no-workers"),
176
+ }
110
177
 
111
- /**
112
- * The FQDN to register the cluster nodes with.
113
- *
114
- * If provided and `registerFqdn` is set to `true`, the corresponding DNS provider must be provided to set up the DNS records.
115
- *
116
- * @schema
117
- */
118
- fqdn: Type.Optional(Type.String()),
178
+ export const clusterInputs = {
179
+ masters: {
180
+ entity: serverEntity,
181
+ multiple: true,
182
+ },
183
+ workers: {
184
+ entity: serverEntity,
185
+ multiple: true,
186
+ required: false,
187
+ },
188
+ } as const
119
189
 
120
- /**
121
- * Whether to register the cluster nodes with the provided FQDN.
122
- *
123
- * By default, `true`.
124
- *
125
- * @schema
126
- */
127
- registerFqdn: Type.Boolean({ default: true }),
128
- }
190
+ export const clusterOutputs = {
191
+ k8sCluster: clusterEntity,
192
+ apiEndpoints: {
193
+ entity: l4EndpointEntity,
194
+ multiple: true,
195
+ },
196
+ endpoints: {
197
+ entity: l3EndpointEntity,
198
+ multiple: true,
199
+ },
200
+ } as const
129
201
 
130
202
  export const existingCluster = defineUnit({
131
203
  type: "k8s.existing-cluster",
132
204
 
133
205
  args: {
134
- ...sharedClusterArgs,
206
+ /**
207
+ * The list of external IPs of the cluster nodes allowed to be used for external access.
208
+ *
209
+ * If not provided, will be automatically detected by querying the cluster nodes.
210
+ *
211
+ * @schema
212
+ */
213
+ externalIps: Type.Optional(Type.Array(Type.String())),
135
214
 
136
215
  /**
137
- * The policy for using the tun device inside containers.
216
+ * The policy for using internal IPs of the nodes as external IPs.
138
217
  *
139
- * If not provided, the default policy is `host` which assumes just mounting /dev/net/tun from the host.
218
+ * - `always`: always use internal IPs as external IPs;
219
+ * - `public`: use internal IPs as external IPs only if they are (theoretically) routable from the public internet **(default)**;
220
+ * - `never`: never use internal IPs as external IPs.
140
221
  *
141
- * For some runtimes, like Talos's one, the /dev/net/tun device is not available in the host, so the plugin policy should be used.
222
+ * @schema
223
+ */
224
+ internalIpsPolicy: Type.Default(internalIpsPolicySchema, "public"),
225
+
226
+ /**
227
+ * The extra quirks of the cluster to improve compatibility.
142
228
  *
143
229
  * @schema
144
230
  */
145
- tunDevicePolicy: Type.Optional(tunDevicePolicySchema),
231
+ quirks: Type.Optional(clusterQuirksSchema),
146
232
  },
147
233
 
148
234
  secrets: {
@@ -156,14 +242,13 @@ export const existingCluster = defineUnit({
156
242
  kubeconfig: Type.Record(Type.String(), Type.Any()),
157
243
  },
158
244
 
159
- outputs: {
160
- cluster: clusterEntity,
161
- },
245
+ outputs: clusterOutputs,
162
246
 
163
247
  meta: {
164
248
  displayName: "Existing Cluster",
165
249
  description: "An existing Kubernetes cluster.",
166
- primaryIcon: "mdi:kubernetes",
250
+ primaryIcon: "devicon:kubernetes",
251
+ category: "Kubernetes",
167
252
  },
168
253
 
169
254
  source: {
@@ -172,16 +257,117 @@ export const existingCluster = defineUnit({
172
257
  },
173
258
  })
174
259
 
260
+ export const clusterPatch = defineUnit({
261
+ type: "k8s.cluster-patch",
262
+
263
+ args: {
264
+ /**
265
+ * The endpoints of the API server.
266
+ *
267
+ * The entry may represent real node endpoint or virtual endpoint (like a load balancer).
268
+ *
269
+ * The same node may also be represented by multiple entries (e.g. a node with private and public IP).
270
+ *
271
+ * @schema
272
+ */
273
+ apiEndpoints: Type.Default(Type.Array(Type.String()), []),
274
+
275
+ /**
276
+ * The mode to use for patching the API endpoints.
277
+ *
278
+ * - `prepend`: prepend the new endpoints to the existing ones (default);
279
+ * - `replace`: replace the existing endpoints with the new ones.
280
+ */
281
+ apiEndpointsPatchMode: Type.Default(arrayPatchModeSchema, "prepend"),
282
+
283
+ /**
284
+ * The endpoints of the cluster nodes.
285
+ *
286
+ * The entry may represent real node endpoint or virtual endpoint (like a load balancer).
287
+ *
288
+ * The same node may also be represented by multiple entries (e.g. a node with private and public IP).
289
+ *
290
+ * @schema
291
+ */
292
+ endpoints: Type.Default(Type.Array(Type.String()), []),
293
+
294
+ /**
295
+ * The mode to use for patching the endpoints.
296
+ *
297
+ * - `prepend`: prepend the new endpoints to the existing ones (default);
298
+ * - `replace`: replace the existing endpoints with the new ones.
299
+ */
300
+ endpointsPatchMode: Type.Default(arrayPatchModeSchema, "prepend"),
301
+ },
302
+
303
+ inputs: {
304
+ k8sCluster: clusterEntity,
305
+ apiEndpoints: {
306
+ entity: l4EndpointEntity,
307
+ required: false,
308
+ multiple: true,
309
+ },
310
+ endpoints: {
311
+ entity: l3EndpointEntity,
312
+ required: false,
313
+ multiple: true,
314
+ },
315
+ },
316
+
317
+ outputs: clusterOutputs,
318
+
319
+ meta: {
320
+ displayName: "Cluster Patch",
321
+ description: "Patches some properties of the cluster.",
322
+ primaryIcon: "devicon:kubernetes",
323
+ secondaryIcon: "fluent:patch-20-filled",
324
+ category: "Kubernetes",
325
+ },
326
+
327
+ source: {
328
+ package: "@highstate/k8s",
329
+ path: "units/cluster-patch",
330
+ },
331
+ })
332
+
333
+ export const clusterDns = defineUnit({
334
+ type: "cluster-dns",
335
+
336
+ args: {
337
+ ...dns.createArgs(),
338
+ ...dns.createArgs("api"),
339
+ },
340
+
341
+ inputs: {
342
+ k8sCluster: clusterEntity,
343
+ ...dns.inputs,
344
+ },
345
+
346
+ outputs: clusterOutputs,
347
+
348
+ meta: {
349
+ displayName: "Cluster DNS",
350
+ description: "Creates DNS records for the cluster and updates endpoints.",
351
+ primaryIcon: "devicon:kubernetes",
352
+ secondaryIcon: "mdi:dns",
353
+ category: "Kubernetes",
354
+ },
355
+
356
+ source: {
357
+ package: "@highstate/k8s",
358
+ path: "units/cluster-dns",
359
+ },
360
+ })
361
+
175
362
  export const gatewayEntity = defineEntity({
176
363
  type: "k8s.gateway",
177
364
 
178
365
  schema: Type.Object({
179
- clusterInfo: clusterInfoSchema,
366
+ clusterId: Type.String(),
180
367
  gatewayClassName: Type.String(),
181
368
  httpListenerPort: Type.Number(),
182
369
  httpsListenerPort: Type.Number(),
183
- ip: Type.String(),
184
- service: Type.Optional(serviceEntity.schema),
370
+ endpoints: Type.Array(l3EndpointEntity.schema),
185
371
  }),
186
372
 
187
373
  meta: {
@@ -193,7 +379,7 @@ export const tlsIssuerEntity = defineEntity({
193
379
  type: "k8s.tls-issuer",
194
380
 
195
381
  schema: Type.Object({
196
- clusterInfo: clusterInfoSchema,
382
+ clusterId: Type.String(),
197
383
  clusterIssuerName: Type.String(),
198
384
  }),
199
385
 
@@ -206,9 +392,10 @@ export const accessPointEntity = defineEntity({
206
392
  type: "k8s.access-point",
207
393
 
208
394
  schema: Type.Object({
395
+ clusterId: Type.String(),
209
396
  gateway: gatewayEntity.schema,
210
397
  tlsIssuer: tlsIssuerEntity.schema,
211
- dnsProviders: Type.Array(providerEntity.schema),
398
+ dnsProviders: Type.Array(dns.providerEntity.schema),
212
399
  }),
213
400
 
214
401
  meta: {
@@ -223,7 +410,7 @@ export const accessPoint = defineUnit({
223
410
  gateway: gatewayEntity,
224
411
  tlsIssuer: tlsIssuerEntity,
225
412
  dnsProviders: {
226
- entity: providerEntity,
413
+ entity: dns.providerEntity,
227
414
  multiple: true,
228
415
  },
229
416
  },
@@ -236,6 +423,7 @@ export const accessPoint = defineUnit({
236
423
  displayName: "Access Point",
237
424
  description: "An access point which can be used to connect to services.",
238
425
  primaryIcon: "mdi:access-point",
426
+ category: "Kubernetes",
239
427
  },
240
428
 
241
429
  source: {
@@ -259,6 +447,7 @@ export const certManager = defineUnit({
259
447
  displayName: "Cert Manager",
260
448
  description: "A certificate manager for managing TLS certificates.",
261
449
  primaryIcon: "simple-icons:letsencrypt",
450
+ category: "Kubernetes",
262
451
  },
263
452
 
264
453
  source: {
@@ -284,7 +473,7 @@ export const dns01TlsIssuer = defineUnit({
284
473
  inputs: {
285
474
  k8sCluster: clusterEntity,
286
475
  dnsProviders: {
287
- entity: providerEntity,
476
+ entity: dns.providerEntity,
288
477
  multiple: true,
289
478
  },
290
479
  },
@@ -297,6 +486,7 @@ export const dns01TlsIssuer = defineUnit({
297
486
  displayName: "DNS01 Issuer",
298
487
  description: "A TLS issuer for issuing certificate using DNS01 challenge.",
299
488
  primaryIcon: "mdi:certificate",
489
+ category: "Kubernetes",
300
490
  },
301
491
 
302
492
  source: {
@@ -305,34 +495,12 @@ export const dns01TlsIssuer = defineUnit({
305
495
  },
306
496
  })
307
497
 
308
- export const containerSchema = Type.Object({
309
- name: Type.String(),
310
- image: Type.String(),
311
- })
312
-
313
- export const labelSelectorSchema = Type.Object({
314
- matchLabels: Type.Record(Type.String(), Type.String()),
315
- })
316
-
317
- export const deploymentSpecSchema = Type.Object({
318
- replicas: Type.Number(),
319
- selector: labelSelectorSchema,
320
- template: Type.Object({
321
- metadata: metadataSchema,
322
- spec: Type.Object({
323
- containers: Type.Array(containerSchema),
324
- }),
325
- }),
326
- })
327
-
328
498
  export const deploymentEntity = defineEntity({
329
499
  type: "k8s.deployment",
330
500
 
331
501
  schema: Type.Object({
332
502
  type: Type.Literal("k8s.deployment"),
333
- clusterInfo: clusterInfoSchema,
334
- metadata: metadataSchema,
335
-
503
+ ...resourceSchema.properties,
336
504
  service: Type.Optional(serviceEntity.schema),
337
505
  }),
338
506
 
@@ -346,10 +514,8 @@ export const statefulSetEntity = defineEntity({
346
514
 
347
515
  schema: Type.Object({
348
516
  type: Type.Literal("k8s.stateful-set"),
349
- clusterInfo: clusterInfoSchema,
350
- metadata: metadataSchema,
351
-
352
- service: Type.Optional(serviceEntity.schema),
517
+ ...resourceSchema.properties,
518
+ service: serviceEntity.schema,
353
519
  }),
354
520
 
355
521
  meta: {
@@ -357,13 +523,22 @@ export const statefulSetEntity = defineEntity({
357
523
  },
358
524
  })
359
525
 
526
+ export const exposableWorkloadEntity = defineEntity({
527
+ type: "k8s.exposable-workload",
528
+
529
+ schema: Type.Union([deploymentEntity.schema, statefulSetEntity.schema]),
530
+
531
+ meta: {
532
+ color: "#4CAF50",
533
+ },
534
+ })
535
+
360
536
  export const persistentVolumeClaimEntity = defineEntity({
361
537
  type: "k8s.persistent-volume-claim",
362
538
 
363
539
  schema: Type.Object({
364
540
  type: Type.Literal("k8s.persistent-volume-claim"),
365
- clusterInfo: clusterInfoSchema,
366
- metadata: metadataSchema,
541
+ ...resourceSchema.properties,
367
542
  }),
368
543
 
369
544
  meta: {
@@ -376,7 +551,7 @@ export const interfaceEntity = defineEntity({
376
551
 
377
552
  schema: Type.Object({
378
553
  name: Type.String(),
379
- deployment: deploymentEntity.schema,
554
+ workload: exposableWorkloadEntity.schema,
380
555
  }),
381
556
 
382
557
  meta: {
@@ -400,8 +575,10 @@ export const gatewayApi = defineUnit({
400
575
  meta: {
401
576
  displayName: "Gateway API",
402
577
  description: "Installs the Gateway API CRDs to the cluster.",
403
- primaryIcon: "mdi:kubernetes",
404
- primaryIconColor: "#4CAF50",
578
+ primaryIcon: "devicon:kubernetes",
579
+ secondaryIcon: "mdi:api",
580
+ secondaryIconColor: "#4CAF50",
581
+ category: "Kubernetes",
405
582
  },
406
583
 
407
584
  source: {
@@ -410,7 +587,7 @@ export const gatewayApi = defineUnit({
410
587
  },
411
588
  })
412
589
 
413
- export type ClusterInfo = Static<typeof clusterInfoSchema>
590
+ export type CNI = Static<typeof cniSchema>
414
591
  export type Cluster = Static<typeof clusterEntity.schema>
415
592
 
416
593
  export type Gateway = Static<typeof gatewayEntity.schema>
@@ -418,15 +595,13 @@ export type TlsIssuer = Static<typeof tlsIssuerEntity.schema>
418
595
  export type AccessPoint = Static<typeof accessPointEntity.schema>
419
596
 
420
597
  export type Metadata = Static<typeof metadataSchema>
598
+ export type Resource = Static<typeof resourceSchema>
421
599
 
422
600
  export type ServiceType = Static<typeof serviceTypeSchema>
423
- export type ServicePort = Static<typeof servicePortSchema>
424
- export type ServiceSpec = Static<typeof serviceSpecSchema>
425
601
  export type Service = Static<typeof serviceEntity.schema>
426
602
 
427
- export type Container = Static<typeof containerSchema>
428
- export type DeploymentSpec = Static<typeof deploymentSpecSchema>
429
603
  export type Deployment = Static<typeof deploymentEntity.schema>
604
+ export type ExposableWorkload = Static<typeof exposableWorkloadEntity.schema>
430
605
 
431
606
  export type PersistentVolumeClaim = Static<typeof persistentVolumeClaimEntity.schema>
432
607
  export type StatefulSet = Static<typeof statefulSetEntity.schema>
package/src/mullvad.ts CHANGED
@@ -1,19 +1,12 @@
1
1
  import { defineUnit, Type } from "@highstate/contract"
2
2
  import { networkEntity, peerEntity } from "./wireguard"
3
- import { l4EndpointEntity } from "./common"
4
-
5
- export const endpointType = Type.Union([
6
- Type.Literal("fqdn"),
7
- Type.Literal("ipv4"),
8
- Type.Literal("ipv6"),
9
- ])
3
+ import { l4EndpointEntity } from "./network"
10
4
 
11
5
  export const peer = defineUnit({
12
6
  type: "mullvad.peer",
13
7
 
14
8
  args: {
15
9
  hostname: Type.Optional(Type.String()),
16
- endpointType: Type.Optional({ ...endpointType, default: "fqdn" }),
17
10
 
18
11
  /**
19
12
  * Whether to include Mullvad DNS servers in the peer configuration.
@@ -37,7 +30,11 @@ export const peer = defineUnit({
37
30
 
38
31
  outputs: {
39
32
  peer: peerEntity,
40
- l4Endpoint: l4EndpointEntity,
33
+
34
+ endpoints: {
35
+ entity: l4EndpointEntity,
36
+ multiple: true,
37
+ },
41
38
  },
42
39
 
43
40
  meta: {
@@ -46,6 +43,7 @@ export const peer = defineUnit({
46
43
  primaryIcon: "simple-icons:mullvad",
47
44
  secondaryIcon: "cib:wireguard",
48
45
  secondaryIconColor: "#88171a",
46
+ category: "VPN",
49
47
  },
50
48
 
51
49
  source: {