@highstate/library 0.7.11 → 0.9.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.
- package/dist/highstate.manifest.json +1 -1
- package/dist/index.js +401 -52
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/common.ts +20 -5
- package/src/index.ts +1 -1
- package/src/k3s.ts +8 -0
- package/src/k8s.ts +82 -5
- package/src/mullvad.ts +9 -0
- package/src/obfuscators/index.ts +1 -0
- package/src/obfuscators/phantun.ts +36 -0
- package/src/obfuscators/shared.ts +82 -0
- package/src/wireguard.ts +74 -4
- package/src/xt-wgobfs.ts +0 -49
package/src/common.ts
CHANGED
@@ -15,16 +15,30 @@ export const serverEntity = defineEntity({
|
|
15
15
|
},
|
16
16
|
})
|
17
17
|
|
18
|
-
export const
|
19
|
-
type: "common.endpoint",
|
18
|
+
export const l3EndpointEntity = defineEntity({
|
19
|
+
type: "common.l3-endpoint",
|
20
20
|
|
21
21
|
schema: Type.Object({
|
22
22
|
endpoint: Type.String(),
|
23
23
|
}),
|
24
24
|
|
25
25
|
meta: {
|
26
|
-
color: "#
|
27
|
-
description: "The L3
|
26
|
+
color: "#1B5E20",
|
27
|
+
description: "The L3 endpoint for some service. May be a domain name or an IP address.",
|
28
|
+
},
|
29
|
+
})
|
30
|
+
|
31
|
+
export const l4EndpointEntity = defineEntity({
|
32
|
+
type: "common.l4-endpoint",
|
33
|
+
|
34
|
+
schema: Type.Object({
|
35
|
+
endpoint: Type.String(),
|
36
|
+
port: Type.Number(),
|
37
|
+
}),
|
38
|
+
|
39
|
+
meta: {
|
40
|
+
color: "#F57F17",
|
41
|
+
description: "The L4 endpoint for some service. Extends an L3 endpoint with a port.",
|
28
42
|
},
|
29
43
|
})
|
30
44
|
|
@@ -145,7 +159,8 @@ export const fileEntity = defineEntity({
|
|
145
159
|
})
|
146
160
|
|
147
161
|
export type Server = Static<typeof serverEntity.schema>
|
148
|
-
export type
|
162
|
+
export type L3Endpoint = Static<typeof l3EndpointEntity.schema>
|
163
|
+
export type L4Endpoint = Static<typeof l4EndpointEntity.schema>
|
149
164
|
|
150
165
|
export type File = Static<typeof fileEntity.schema>
|
151
166
|
export type FileMeta = Static<typeof fileMetaEntity.schema>
|
package/src/index.ts
CHANGED
@@ -7,10 +7,10 @@ export * as wireguard from "./wireguard"
|
|
7
7
|
export * as apps from "./apps"
|
8
8
|
export * as cloudflare from "./cloudflare"
|
9
9
|
export * as k3s from "./k3s"
|
10
|
-
// export * as xtWgobfs from "./xt-wgobfs"
|
11
10
|
export * as restic from "./restic"
|
12
11
|
export * as mullvad from "./mullvad"
|
13
12
|
export * as dns from "./dns"
|
14
13
|
export * as timeweb from "./timeweb"
|
15
14
|
export * as nixos from "./nixos"
|
16
15
|
export * as sops from "./sops"
|
16
|
+
export * as obfuscators from "./obfuscators"
|
package/src/k3s.ts
CHANGED
@@ -1,16 +1,24 @@
|
|
1
1
|
import { defineUnit } from "@highstate/contract"
|
2
|
+
import { Type } from "@sinclair/typebox"
|
2
3
|
import { serverEntity } from "./common"
|
3
4
|
import { clusterEntity, sharedClusterArgs } from "./k8s"
|
5
|
+
import { providerEntity } from "./dns"
|
4
6
|
|
5
7
|
export const cluster = defineUnit({
|
6
8
|
type: "k3s.cluster",
|
7
9
|
|
8
10
|
args: {
|
9
11
|
...sharedClusterArgs,
|
12
|
+
config: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
10
13
|
},
|
11
14
|
|
12
15
|
inputs: {
|
13
16
|
server: serverEntity,
|
17
|
+
dnsProviders: {
|
18
|
+
entity: providerEntity,
|
19
|
+
required: false,
|
20
|
+
multiple: true,
|
21
|
+
},
|
14
22
|
},
|
15
23
|
|
16
24
|
outputs: {
|
package/src/k8s.ts
CHANGED
@@ -1,11 +1,35 @@
|
|
1
1
|
import { defineEntity, defineUnit, Type, type Static } from "@highstate/contract"
|
2
|
+
import { Literal } from "@sinclair/typebox"
|
2
3
|
import { providerEntity } from "./dns"
|
3
4
|
|
5
|
+
export const tunDevicePolicySchema = Type.Union([
|
6
|
+
Type.Object({
|
7
|
+
type: Literal("host"),
|
8
|
+
}),
|
9
|
+
Type.Object({
|
10
|
+
type: Literal("plugin"),
|
11
|
+
resourceName: Type.String(),
|
12
|
+
resourceValue: Type.String(),
|
13
|
+
}),
|
14
|
+
])
|
15
|
+
|
4
16
|
export const clusterInfoSchema = Type.Object({
|
5
17
|
id: Type.String(),
|
6
18
|
name: Type.String(),
|
7
19
|
cni: Type.Optional(Type.String()),
|
8
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
|
+
|
25
|
+
/**
|
26
|
+
* Specifies the policy for using the tun device inside containers.
|
27
|
+
*
|
28
|
+
* If not provided, the default policy is `host` which assumes just mounting /dev/net/tun from the host.
|
29
|
+
*
|
30
|
+
* 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.
|
31
|
+
*/
|
32
|
+
tunDevicePolicy: Type.Optional(tunDevicePolicySchema),
|
9
33
|
})
|
10
34
|
|
11
35
|
export const serviceTypeSchema = Type.StringEnum(["NodePort", "LoadBalancer", "ClusterIP"])
|
@@ -108,6 +132,17 @@ export const existingCluster = defineUnit({
|
|
108
132
|
|
109
133
|
args: {
|
110
134
|
...sharedClusterArgs,
|
135
|
+
|
136
|
+
/**
|
137
|
+
* The policy for using the tun device inside containers.
|
138
|
+
*
|
139
|
+
* If not provided, the default policy is `host` which assumes just mounting /dev/net/tun from the host.
|
140
|
+
*
|
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.
|
142
|
+
*
|
143
|
+
* @schema
|
144
|
+
*/
|
145
|
+
tunDevicePolicy: Type.Optional(tunDevicePolicySchema),
|
111
146
|
},
|
112
147
|
|
113
148
|
secrets: {
|
@@ -168,15 +203,16 @@ export const tlsIssuerEntity = defineEntity({
|
|
168
203
|
})
|
169
204
|
|
170
205
|
export const accessPointEntity = defineEntity({
|
171
|
-
type: "
|
206
|
+
type: "k8s.access-point",
|
207
|
+
|
172
208
|
schema: Type.Object({
|
173
209
|
gateway: gatewayEntity.schema,
|
174
210
|
tlsIssuer: tlsIssuerEntity.schema,
|
175
|
-
|
211
|
+
dnsProviders: Type.Array(providerEntity.schema),
|
176
212
|
}),
|
177
213
|
|
178
214
|
meta: {
|
179
|
-
color: "#
|
215
|
+
color: "#F57F17",
|
180
216
|
},
|
181
217
|
})
|
182
218
|
|
@@ -186,7 +222,10 @@ export const accessPoint = defineUnit({
|
|
186
222
|
inputs: {
|
187
223
|
gateway: gatewayEntity,
|
188
224
|
tlsIssuer: tlsIssuerEntity,
|
189
|
-
|
225
|
+
dnsProviders: {
|
226
|
+
entity: providerEntity,
|
227
|
+
multiple: true,
|
228
|
+
},
|
190
229
|
},
|
191
230
|
|
192
231
|
outputs: {
|
@@ -231,9 +270,23 @@ export const certManager = defineUnit({
|
|
231
270
|
export const dns01TlsIssuer = defineUnit({
|
232
271
|
type: "k8s.dns01-issuer",
|
233
272
|
|
273
|
+
args: {
|
274
|
+
/**
|
275
|
+
* The top-level domains to filter the DNS01 challenge for.
|
276
|
+
*
|
277
|
+
* If not provided, will use all domains passed to the DNS providers.
|
278
|
+
*
|
279
|
+
* @schema
|
280
|
+
*/
|
281
|
+
domains: Type.Optional(Type.Array(Type.String())),
|
282
|
+
},
|
283
|
+
|
234
284
|
inputs: {
|
235
285
|
k8sCluster: clusterEntity,
|
236
|
-
|
286
|
+
dnsProviders: {
|
287
|
+
entity: providerEntity,
|
288
|
+
multiple: true,
|
289
|
+
},
|
237
290
|
},
|
238
291
|
|
239
292
|
outputs: {
|
@@ -333,6 +386,30 @@ export const interfaceEntity = defineEntity({
|
|
333
386
|
},
|
334
387
|
})
|
335
388
|
|
389
|
+
export const gatewayApi = defineUnit({
|
390
|
+
type: "k8s.gateway-api",
|
391
|
+
|
392
|
+
inputs: {
|
393
|
+
k8sCluster: clusterEntity,
|
394
|
+
},
|
395
|
+
|
396
|
+
outputs: {
|
397
|
+
k8sCluster: clusterEntity,
|
398
|
+
},
|
399
|
+
|
400
|
+
meta: {
|
401
|
+
displayName: "Gateway API",
|
402
|
+
description: "Installs the Gateway API CRDs to the cluster.",
|
403
|
+
primaryIcon: "mdi:kubernetes",
|
404
|
+
primaryIconColor: "#4CAF50",
|
405
|
+
},
|
406
|
+
|
407
|
+
source: {
|
408
|
+
package: "@highstate/k8s",
|
409
|
+
path: "units/gateway-api",
|
410
|
+
},
|
411
|
+
})
|
412
|
+
|
336
413
|
export type ClusterInfo = Static<typeof clusterInfoSchema>
|
337
414
|
export type Cluster = Static<typeof clusterEntity.schema>
|
338
415
|
|
package/src/mullvad.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { defineUnit, Type } from "@highstate/contract"
|
2
2
|
import { networkEntity, peerEntity } from "./wireguard"
|
3
|
+
import { l4EndpointEntity } from "./common"
|
3
4
|
|
4
5
|
export const endpointType = Type.Union([
|
5
6
|
Type.Literal("fqdn"),
|
@@ -13,6 +14,13 @@ export const peer = defineUnit({
|
|
13
14
|
args: {
|
14
15
|
hostname: Type.Optional(Type.String()),
|
15
16
|
endpointType: Type.Optional({ ...endpointType, default: "fqdn" }),
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Whether to include Mullvad DNS servers in the peer configuration.
|
20
|
+
*
|
21
|
+
* @schema
|
22
|
+
*/
|
23
|
+
includeDns: Type.Default(Type.Boolean(), true),
|
16
24
|
},
|
17
25
|
|
18
26
|
inputs: {
|
@@ -29,6 +37,7 @@ export const peer = defineUnit({
|
|
29
37
|
|
30
38
|
outputs: {
|
31
39
|
peer: peerEntity,
|
40
|
+
l4Endpoint: l4EndpointEntity,
|
32
41
|
},
|
33
42
|
|
34
43
|
meta: {
|
@@ -0,0 +1 @@
|
|
1
|
+
export * as phantun from "./phantun"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { defineUnit } from "@highstate/contract"
|
2
|
+
import { deobfuscatorSpec, obfuscatorSpec } from "./shared"
|
3
|
+
|
4
|
+
export const deobfuscator = defineUnit({
|
5
|
+
type: "obfuscators.phantun.deobfuscator",
|
6
|
+
...deobfuscatorSpec,
|
7
|
+
|
8
|
+
meta: {
|
9
|
+
displayName: "Phantun Deobfuscator",
|
10
|
+
description: "The Phantun Deobfuscator deployed on Kubernetes.",
|
11
|
+
primaryIcon: "mdi:network-outline",
|
12
|
+
secondaryIcon: "mdi:hide",
|
13
|
+
},
|
14
|
+
|
15
|
+
source: {
|
16
|
+
package: "@highstate/obfuscators",
|
17
|
+
path: "phantun/deobfuscator",
|
18
|
+
},
|
19
|
+
})
|
20
|
+
|
21
|
+
export const obfuscator = defineUnit({
|
22
|
+
type: "obfuscators.phantun.obfuscator",
|
23
|
+
...obfuscatorSpec,
|
24
|
+
|
25
|
+
meta: {
|
26
|
+
displayName: "Phantun Obfuscator",
|
27
|
+
description: "The Phantun Obfuscator deployed on Kubernetes.",
|
28
|
+
primaryIcon: "mdi:network-outline",
|
29
|
+
secondaryIcon: "mdi:hide",
|
30
|
+
},
|
31
|
+
|
32
|
+
source: {
|
33
|
+
package: "@highstate/obfuscators",
|
34
|
+
path: "phantun/obfuscator",
|
35
|
+
},
|
36
|
+
})
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
2
|
+
import { clusterEntity } from "../k8s"
|
3
|
+
import { l4EndpointEntity } from "../common"
|
4
|
+
|
5
|
+
export const deobfuscatorSpec = {
|
6
|
+
args: {
|
7
|
+
/**
|
8
|
+
* The L4 endpoint to forward deobfuscated traffic to.
|
9
|
+
*
|
10
|
+
* Will take precedence over the `targetEndpoint` input.
|
11
|
+
*
|
12
|
+
* @schema
|
13
|
+
*/
|
14
|
+
targetEndpoint: Type.Optional(Type.String()),
|
15
|
+
},
|
16
|
+
|
17
|
+
inputs: {
|
18
|
+
/**
|
19
|
+
* The Kubernetes cluster to deploy the deobfuscator on.
|
20
|
+
*
|
21
|
+
* @schema
|
22
|
+
*/
|
23
|
+
k8sCluster: clusterEntity,
|
24
|
+
|
25
|
+
/**
|
26
|
+
* The L4 endpoint to forward deobfuscated traffic to.
|
27
|
+
*
|
28
|
+
* @schema
|
29
|
+
*/
|
30
|
+
targetEndpoint: l4EndpointEntity,
|
31
|
+
},
|
32
|
+
|
33
|
+
outputs: {
|
34
|
+
/**
|
35
|
+
* The L4 endpoint of the deobfuscator accepting obfuscated traffic.
|
36
|
+
*
|
37
|
+
* @schema
|
38
|
+
*/
|
39
|
+
endpoint: l4EndpointEntity,
|
40
|
+
},
|
41
|
+
}
|
42
|
+
|
43
|
+
export const obfuscatorSpec = {
|
44
|
+
args: {
|
45
|
+
/**
|
46
|
+
* The endpoint of the deobfuscator to pass obfuscated traffic to.
|
47
|
+
*
|
48
|
+
* Will take precedence over the `l4Endpoint` input.
|
49
|
+
*
|
50
|
+
* @schema
|
51
|
+
*/
|
52
|
+
endpoint: Type.Optional(Type.String()),
|
53
|
+
},
|
54
|
+
|
55
|
+
inputs: {
|
56
|
+
/**
|
57
|
+
* The Kubernetes cluster to deploy the obfuscator on.
|
58
|
+
*
|
59
|
+
* @schema
|
60
|
+
*/
|
61
|
+
k8sCluster: clusterEntity,
|
62
|
+
|
63
|
+
/**
|
64
|
+
* The L4 endpoint of the deobfuscator to pass obfuscated traffic to.
|
65
|
+
*
|
66
|
+
* @schema
|
67
|
+
*/
|
68
|
+
endpoint: {
|
69
|
+
entity: l4EndpointEntity,
|
70
|
+
required: false,
|
71
|
+
},
|
72
|
+
},
|
73
|
+
|
74
|
+
outputs: {
|
75
|
+
/**
|
76
|
+
* The L4 endpoint accepting unobfuscated traffic.
|
77
|
+
*
|
78
|
+
* @schema
|
79
|
+
*/
|
80
|
+
entryEndpoint: l4EndpointEntity,
|
81
|
+
},
|
82
|
+
}
|
package/src/wireguard.ts
CHANGED
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
statefulSetEntity,
|
9
9
|
} from "./k8s"
|
10
10
|
import { providerEntity } from "./dns"
|
11
|
+
import { l4EndpointEntity } from "./common"
|
11
12
|
|
12
13
|
export const backendSchema = Type.StringEnum(["wireguard", "amneziawg"])
|
13
14
|
export const presharedKeyModeSchema = Type.StringEnum(["none", "global", "secure"])
|
@@ -101,7 +102,7 @@ export const network = defineUnit({
|
|
101
102
|
*
|
102
103
|
* @schema
|
103
104
|
*/
|
104
|
-
backend: backendSchema,
|
105
|
+
backend: Type.Default(backendSchema, "wireguard"),
|
105
106
|
|
106
107
|
/**
|
107
108
|
* The option which defines how to handle pre-shared keys between peers.
|
@@ -284,7 +285,7 @@ export const peer = defineUnit({
|
|
284
285
|
*
|
285
286
|
* @schema
|
286
287
|
*/
|
287
|
-
publicKey: Type.String(),
|
288
|
+
publicKey: Type.Optional(Type.String()),
|
288
289
|
},
|
289
290
|
|
290
291
|
inputs: {
|
@@ -299,6 +300,28 @@ export const peer = defineUnit({
|
|
299
300
|
entity: networkEntity,
|
300
301
|
required: false,
|
301
302
|
},
|
303
|
+
|
304
|
+
/**
|
305
|
+
* The existing WireGuard peer to extend.
|
306
|
+
*
|
307
|
+
* @schema
|
308
|
+
*/
|
309
|
+
peer: {
|
310
|
+
entity: peerEntity,
|
311
|
+
required: false,
|
312
|
+
},
|
313
|
+
|
314
|
+
/**
|
315
|
+
* The L4 endpoint of the peer.
|
316
|
+
*
|
317
|
+
* Will take priority over all calculated endpoints if provided.
|
318
|
+
*
|
319
|
+
* @schema
|
320
|
+
*/
|
321
|
+
l4Endpoint: {
|
322
|
+
entity: l4EndpointEntity,
|
323
|
+
required: false,
|
324
|
+
},
|
302
325
|
},
|
303
326
|
|
304
327
|
outputs: {
|
@@ -349,6 +372,8 @@ export const identity = defineUnit({
|
|
349
372
|
*
|
350
373
|
* If overridden, does not affect node which implements the identity, but is used in the peer configuration of other nodes.
|
351
374
|
*
|
375
|
+
* Will take priority over all calculated endpoints and `l4Endpoint` input.
|
376
|
+
*
|
352
377
|
* @schema
|
353
378
|
*/
|
354
379
|
endpoint: Type.Optional(Type.String()),
|
@@ -357,11 +382,21 @@ export const identity = defineUnit({
|
|
357
382
|
* The FQDN of the WireGuard identity.
|
358
383
|
* Will be used as endpoint for the peer.
|
359
384
|
*
|
360
|
-
* If `dnsProvider` is provided and `
|
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.
|
361
387
|
*
|
362
388
|
* @schema
|
363
389
|
*/
|
364
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),
|
365
400
|
},
|
366
401
|
|
367
402
|
secrets: {
|
@@ -410,15 +445,50 @@ export const identity = defineUnit({
|
|
410
445
|
required: false,
|
411
446
|
},
|
412
447
|
|
413
|
-
|
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: {
|
414
478
|
entity: providerEntity,
|
415
479
|
required: false,
|
480
|
+
multiple: true,
|
416
481
|
},
|
417
482
|
},
|
418
483
|
|
419
484
|
outputs: {
|
420
485
|
identity: identityEntity,
|
421
486
|
peer: peerEntity,
|
487
|
+
|
488
|
+
l4Endpoint: {
|
489
|
+
entity: l4EndpointEntity,
|
490
|
+
required: false,
|
491
|
+
},
|
422
492
|
},
|
423
493
|
|
424
494
|
meta: {
|
package/src/xt-wgobfs.ts
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import { defineEntity, defineUnit, Type } from "@highstate/contract"
|
2
|
-
import { endpointEntity } from "./common"
|
3
|
-
|
4
|
-
export const channelEntity = defineEntity({
|
5
|
-
type: "xt-wgobfs.target",
|
6
|
-
|
7
|
-
schema: Type.Object({
|
8
|
-
endpoint: Type.String(),
|
9
|
-
}),
|
10
|
-
})
|
11
|
-
|
12
|
-
export const obfuscatorNode = defineUnit({
|
13
|
-
type: "xt-wgobfs.obfuscator",
|
14
|
-
|
15
|
-
outputs: {
|
16
|
-
outerCircuit: endpointEntity,
|
17
|
-
channel: channelEntity,
|
18
|
-
},
|
19
|
-
|
20
|
-
source: {
|
21
|
-
package: "@highstate/xt-wgobfs",
|
22
|
-
path: "target-node",
|
23
|
-
},
|
24
|
-
|
25
|
-
meta: {
|
26
|
-
displayName: "xt-wgobfs Deobfuscator",
|
27
|
-
},
|
28
|
-
})
|
29
|
-
|
30
|
-
export const deobfuscatorNode = defineUnit({
|
31
|
-
type: "xt-wgobfs.deobfuscator",
|
32
|
-
|
33
|
-
inputs: {
|
34
|
-
channel: channelEntity,
|
35
|
-
},
|
36
|
-
|
37
|
-
outputs: {
|
38
|
-
outerCircuit: endpointEntity,
|
39
|
-
},
|
40
|
-
|
41
|
-
source: {
|
42
|
-
package: "@highstate/xt-wgobfs",
|
43
|
-
path: "source-node",
|
44
|
-
},
|
45
|
-
|
46
|
-
meta: {
|
47
|
-
displayName: "xt-wgobfs Obfuscator",
|
48
|
-
},
|
49
|
-
})
|