@opensecurity/zonzon-cli 0.1.2 → 0.1.4
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/cli.js +58 -31
- package/package.json +9 -5
- package/src/cli.ts +0 -284
- package/tsconfig.json +0 -13
package/dist/cli.js
CHANGED
|
@@ -84,6 +84,8 @@ async function handleInit(configPath) {
|
|
|
84
84
|
}
|
|
85
85
|
const defaultConf = {
|
|
86
86
|
port: 53,
|
|
87
|
+
httpPort: 80,
|
|
88
|
+
httpsPort: 443,
|
|
87
89
|
fallbackDns: "1.1.1.1",
|
|
88
90
|
maxTcpConnections: 100,
|
|
89
91
|
tcpIdleTimeoutMs: 30000,
|
|
@@ -99,6 +101,8 @@ async function handleInit(configPath) {
|
|
|
99
101
|
};
|
|
100
102
|
saveConfig(configPath, defaultConf);
|
|
101
103
|
audit.system(`Initialized secure default configuration at ${configPath}`);
|
|
104
|
+
audit.system(`Security Notice: Default HTTP/HTTPS ports mapped to 80/443.`);
|
|
105
|
+
audit.system(`If executing within a non-root sandbox, mutate config.json to unprivileged ports (e.g. 8080/8443) to prevent EACCES binding faults.`);
|
|
102
106
|
process.exit(0);
|
|
103
107
|
}
|
|
104
108
|
async function handleConfig(configPath, args) {
|
|
@@ -127,6 +131,45 @@ async function handleConfig(configPath, args) {
|
|
|
127
131
|
}
|
|
128
132
|
printUsage();
|
|
129
133
|
}
|
|
134
|
+
class ZonzonDaemon {
|
|
135
|
+
dnsHandler = null;
|
|
136
|
+
httpHandler = null;
|
|
137
|
+
sniProxy = null;
|
|
138
|
+
async start(config) {
|
|
139
|
+
try {
|
|
140
|
+
const dnsServer = new DevDnsServer(config);
|
|
141
|
+
this.dnsHandler = new DnsHandler(dnsServer, config);
|
|
142
|
+
await this.dnsHandler.start();
|
|
143
|
+
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
144
|
+
this.httpHandler = new HttpHandler(dnsServer, config, config.httpPort ?? 80);
|
|
145
|
+
await this.httpHandler.start();
|
|
146
|
+
audit.system(`HTTP L7 Sandbox Router active on port ${config.httpPort ?? 80}`);
|
|
147
|
+
this.sniProxy = new SniProxyService(config, config.httpsPort ?? 443);
|
|
148
|
+
await this.sniProxy.start();
|
|
149
|
+
audit.system(`SNI Proxy active on port ${config.httpsPort ?? 443}`);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
153
|
+
await this.stop();
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async stop() {
|
|
158
|
+
if (this.dnsHandler) {
|
|
159
|
+
await this.dnsHandler.stop();
|
|
160
|
+
this.dnsHandler = null;
|
|
161
|
+
}
|
|
162
|
+
if (this.httpHandler) {
|
|
163
|
+
await this.httpHandler.stop();
|
|
164
|
+
this.httpHandler = null;
|
|
165
|
+
}
|
|
166
|
+
if (this.sniProxy) {
|
|
167
|
+
await this.sniProxy.stop();
|
|
168
|
+
this.sniProxy = null;
|
|
169
|
+
}
|
|
170
|
+
audit.system("Subsystems halted. Sockets closed.");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
130
173
|
async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
131
174
|
const rawConfig = loadConfig(configPath);
|
|
132
175
|
if (portOverride) {
|
|
@@ -145,10 +188,8 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
|
145
188
|
audit.error(`Configuration Schema Violation: ${err.message}`);
|
|
146
189
|
process.exit(1);
|
|
147
190
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const httpHandler = new HttpHandler(dnsServer, config, 80);
|
|
151
|
-
const sniProxy = new SniProxyService(config, 443);
|
|
191
|
+
const daemon = new ZonzonDaemon();
|
|
192
|
+
await daemon.start(config);
|
|
152
193
|
const isCpEnabled = config.controlPlane?.enabled !== false;
|
|
153
194
|
let controlPlane = null;
|
|
154
195
|
let isEphemeralKey = false;
|
|
@@ -167,15 +208,24 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
|
167
208
|
blindIndexSalt: blindIndexSalt,
|
|
168
209
|
initialConfig: config,
|
|
169
210
|
});
|
|
170
|
-
controlPlane.subscribe((newConfig) => {
|
|
211
|
+
controlPlane.subscribe(async (newConfig) => {
|
|
171
212
|
audit.system("Applying dynamic configuration update from Control Plane...");
|
|
213
|
+
await daemon.stop();
|
|
214
|
+
await daemon.start(newConfig);
|
|
172
215
|
});
|
|
216
|
+
await controlPlane.start();
|
|
217
|
+
if (isEphemeralKey) {
|
|
218
|
+
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
219
|
+
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
223
|
+
}
|
|
173
224
|
}
|
|
225
|
+
audit.system("Initialization complete. Awaiting connections...");
|
|
174
226
|
const shutdown = async () => {
|
|
175
227
|
audit.system("Initiating graceful shutdown sequence...");
|
|
176
|
-
await
|
|
177
|
-
await httpHandler.stop();
|
|
178
|
-
await sniProxy.stop();
|
|
228
|
+
await daemon.stop();
|
|
179
229
|
if (controlPlane) {
|
|
180
230
|
await controlPlane.stop();
|
|
181
231
|
}
|
|
@@ -183,29 +233,6 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
|
183
233
|
};
|
|
184
234
|
process.on("SIGINT", shutdown);
|
|
185
235
|
process.on("SIGTERM", shutdown);
|
|
186
|
-
try {
|
|
187
|
-
await dnsHandler.start();
|
|
188
|
-
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
189
|
-
await httpHandler.start();
|
|
190
|
-
audit.system(`HTTP L7 Sandbox Router active on port 80`);
|
|
191
|
-
await sniProxy.start();
|
|
192
|
-
audit.system(`SNI Proxy active on port 443`);
|
|
193
|
-
if (controlPlane) {
|
|
194
|
-
await controlPlane.start();
|
|
195
|
-
if (isEphemeralKey) {
|
|
196
|
-
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
197
|
-
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
audit.system("Initialization complete. Awaiting connections...");
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
207
|
-
await shutdown();
|
|
208
|
-
}
|
|
209
236
|
}
|
|
210
237
|
async function main() {
|
|
211
238
|
const { values, positionals } = parseArgs({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensecurity/zonzon-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "cli interface for zonzon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Lucian BLETAN <neuraluc@gmail.com>",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -22,12 +22,16 @@
|
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
25
28
|
"scripts": {
|
|
26
29
|
"build": "tsc -b",
|
|
27
|
-
"start": "node
|
|
30
|
+
"start": "node dist/cli.js",
|
|
31
|
+
"dev:watch": "NODE_OPTIONS=--disable-warning=DEP0205 tsx watch src/cli.ts start --config ../../config/hosts.json"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@opensecurity/zonzon-core": "
|
|
31
|
-
"@opensecurity/zonzon-control-plane": "
|
|
34
|
+
"@opensecurity/zonzon-core": "^0.1.4",
|
|
35
|
+
"@opensecurity/zonzon-control-plane": "^0.1.4"
|
|
32
36
|
}
|
|
33
37
|
}
|
package/src/cli.ts
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import * as os from "os";
|
|
6
|
-
import { parseArgs } from "util";
|
|
7
|
-
import { randomBytes } from "crypto";
|
|
8
|
-
import {
|
|
9
|
-
DevDnsServer,
|
|
10
|
-
DnsHandler,
|
|
11
|
-
HttpHandler,
|
|
12
|
-
SniProxyService,
|
|
13
|
-
ServerConfig,
|
|
14
|
-
validateServerConfig,
|
|
15
|
-
audit
|
|
16
|
-
} from "@opensecurity/zonzon-core";
|
|
17
|
-
import { ControlPlane } from "@opensecurity/zonzon-control-plane";
|
|
18
|
-
|
|
19
|
-
const CONFIG_DIR = path.join(os.homedir(), ".zonzon");
|
|
20
|
-
const DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
21
|
-
|
|
22
|
-
function ensureConfigDir() {
|
|
23
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
24
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function loadConfig(configPath: string): any {
|
|
29
|
-
if (!fs.existsSync(configPath)) {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
34
|
-
return JSON.parse(fileContents) || {};
|
|
35
|
-
} catch (err: any) {
|
|
36
|
-
audit.error(`Failed to parse configuration file at ${configPath}: ${err.message}`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function saveConfig(configPath: string, data: any): void {
|
|
42
|
-
ensureConfigDir();
|
|
43
|
-
try {
|
|
44
|
-
const jsonStr = JSON.stringify(data, null, 2);
|
|
45
|
-
fs.writeFileSync(configPath, jsonStr, { encoding: "utf8", mode: 0o600 });
|
|
46
|
-
} catch (err: any) {
|
|
47
|
-
audit.error(`Failed to write configuration file at ${configPath}: ${err.message}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function setDeepValue(obj: any, pathStr: string, value: any): void {
|
|
53
|
-
const parts = pathStr.split(".");
|
|
54
|
-
const last = parts.pop()!;
|
|
55
|
-
let current = obj;
|
|
56
|
-
for (const part of parts) {
|
|
57
|
-
if (!current[part] || typeof current[part] !== "object") {
|
|
58
|
-
current[part] = {};
|
|
59
|
-
}
|
|
60
|
-
current = current[part];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (value === "true") current[last] = true;
|
|
64
|
-
else if (value === "false") current[last] = false;
|
|
65
|
-
else if (!isNaN(Number(value))) current[last] = Number(value);
|
|
66
|
-
else current[last] = value;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function printUsage() {
|
|
70
|
-
console.log(`
|
|
71
|
-
zonzon core engine (v0.1.0)
|
|
72
|
-
Usage: zonzon <command> [options]
|
|
73
|
-
|
|
74
|
-
Commands:
|
|
75
|
-
init Initialize the default configuration file at ~/.zonzon/config.json
|
|
76
|
-
start Boot the routing engine and control plane
|
|
77
|
-
config Manage configuration state
|
|
78
|
-
|
|
79
|
-
Config Commands:
|
|
80
|
-
zonzon config view Print the current configuration
|
|
81
|
-
zonzon config set <key> <value> Set a configuration value using dot notation
|
|
82
|
-
Example: zonzon config set port 53
|
|
83
|
-
Example: zonzon config set controlPlane.port 8081
|
|
84
|
-
|
|
85
|
-
Global Options:
|
|
86
|
-
--config, -c Override path to configuration file (default: ~/.zonzon/config.json)
|
|
87
|
-
`);
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function handleInit(configPath: string) {
|
|
92
|
-
if (fs.existsSync(configPath)) {
|
|
93
|
-
audit.error(`Configuration already exists at ${configPath}`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const defaultConf = {
|
|
98
|
-
port: 53,
|
|
99
|
-
fallbackDns: "1.1.1.1",
|
|
100
|
-
maxTcpConnections: 100,
|
|
101
|
-
tcpIdleTimeoutMs: 30000,
|
|
102
|
-
controlPlane: {
|
|
103
|
-
enabled: true,
|
|
104
|
-
port: 8080
|
|
105
|
-
},
|
|
106
|
-
firewall: {
|
|
107
|
-
defaultPolicy: "deny",
|
|
108
|
-
allowlist_ips: ["127.0.0.1"]
|
|
109
|
-
},
|
|
110
|
-
hosts: {}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
saveConfig(configPath, defaultConf);
|
|
114
|
-
audit.system(`Initialized secure default configuration at ${configPath}`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function handleConfig(configPath: string, args: string[]) {
|
|
119
|
-
const subCmd = args[0];
|
|
120
|
-
if (subCmd === "view") {
|
|
121
|
-
if (!fs.existsSync(configPath)) {
|
|
122
|
-
audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
126
|
-
console.log(fileContents);
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (subCmd === "set") {
|
|
131
|
-
const key = args[1];
|
|
132
|
-
const value = args[2];
|
|
133
|
-
if (!key || value === undefined) {
|
|
134
|
-
audit.error("Usage: zonzon config set <key> <value>");
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const currentConfig = loadConfig(configPath);
|
|
139
|
-
setDeepValue(currentConfig, key, value);
|
|
140
|
-
saveConfig(configPath, currentConfig);
|
|
141
|
-
audit.system(`Updated configuration: ${key} = ${value}`);
|
|
142
|
-
process.exit(0);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
printUsage();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function startEngine(configPath: string, portOverride?: string, cpPortOverride?: string) {
|
|
149
|
-
const rawConfig = loadConfig(configPath);
|
|
150
|
-
|
|
151
|
-
if (portOverride) {
|
|
152
|
-
rawConfig.port = parseInt(portOverride, 10);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (cpPortOverride) {
|
|
156
|
-
if (!rawConfig.controlPlane) rawConfig.controlPlane = {};
|
|
157
|
-
rawConfig.controlPlane.port = parseInt(cpPortOverride, 10);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let config: ServerConfig;
|
|
161
|
-
try {
|
|
162
|
-
config = validateServerConfig(rawConfig);
|
|
163
|
-
} catch (err: any) {
|
|
164
|
-
audit.error(`Configuration Schema Violation: ${err.message}`);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const dnsServer = new DevDnsServer(config);
|
|
169
|
-
const dnsHandler = new DnsHandler(dnsServer, config);
|
|
170
|
-
const httpHandler = new HttpHandler(dnsServer, config, 80);
|
|
171
|
-
const sniProxy = new SniProxyService(config, 443);
|
|
172
|
-
|
|
173
|
-
const isCpEnabled = config.controlPlane?.enabled !== false;
|
|
174
|
-
let controlPlane: ControlPlane | null = null;
|
|
175
|
-
let isEphemeralKey = false;
|
|
176
|
-
let activeApiKey = "";
|
|
177
|
-
|
|
178
|
-
if (isCpEnabled) {
|
|
179
|
-
activeApiKey = config.controlPlane?.apiKey || "";
|
|
180
|
-
if (!activeApiKey) {
|
|
181
|
-
activeApiKey = randomBytes(32).toString("hex");
|
|
182
|
-
isEphemeralKey = true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const blindIndexSalt = randomBytes(16).toString("hex");
|
|
186
|
-
const cpPort = config.controlPlane?.port || 8080;
|
|
187
|
-
|
|
188
|
-
controlPlane = new ControlPlane({
|
|
189
|
-
port: cpPort,
|
|
190
|
-
apiKey: activeApiKey,
|
|
191
|
-
blindIndexSalt: blindIndexSalt,
|
|
192
|
-
initialConfig: config,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
controlPlane.subscribe((newConfig) => {
|
|
196
|
-
audit.system("Applying dynamic configuration update from Control Plane...");
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const shutdown = async () => {
|
|
201
|
-
audit.system("Initiating graceful shutdown sequence...");
|
|
202
|
-
await dnsHandler.stop();
|
|
203
|
-
await httpHandler.stop();
|
|
204
|
-
await sniProxy.stop();
|
|
205
|
-
if (controlPlane) {
|
|
206
|
-
await controlPlane.stop();
|
|
207
|
-
}
|
|
208
|
-
process.exit(0);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
process.on("SIGINT", shutdown);
|
|
212
|
-
process.on("SIGTERM", shutdown);
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await dnsHandler.start();
|
|
216
|
-
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
217
|
-
|
|
218
|
-
await httpHandler.start();
|
|
219
|
-
audit.system(`HTTP L7 Sandbox Router active on port 80`);
|
|
220
|
-
|
|
221
|
-
await sniProxy.start();
|
|
222
|
-
audit.system(`SNI Proxy active on port 443`);
|
|
223
|
-
|
|
224
|
-
if (controlPlane) {
|
|
225
|
-
await controlPlane.start();
|
|
226
|
-
if (isEphemeralKey) {
|
|
227
|
-
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
228
|
-
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
229
|
-
} else {
|
|
230
|
-
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
audit.system("Initialization complete. Awaiting connections...");
|
|
235
|
-
} catch (err: any) {
|
|
236
|
-
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
237
|
-
await shutdown();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function main() {
|
|
242
|
-
const { values, positionals } = parseArgs({
|
|
243
|
-
options: {
|
|
244
|
-
config: {
|
|
245
|
-
type: "string",
|
|
246
|
-
short: "c",
|
|
247
|
-
},
|
|
248
|
-
port: {
|
|
249
|
-
type: "string",
|
|
250
|
-
short: "p",
|
|
251
|
-
},
|
|
252
|
-
"cp-port": {
|
|
253
|
-
type: "string",
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
strict: false,
|
|
257
|
-
allowPositionals: true
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const command = positionals[0];
|
|
261
|
-
const configPath = values.config
|
|
262
|
-
? path.resolve(process.cwd(), values.config as string)
|
|
263
|
-
: DEFAULT_CONFIG_PATH;
|
|
264
|
-
|
|
265
|
-
switch (command) {
|
|
266
|
-
case "init":
|
|
267
|
-
await handleInit(configPath);
|
|
268
|
-
break;
|
|
269
|
-
case "config":
|
|
270
|
-
await handleConfig(configPath, positionals.slice(1));
|
|
271
|
-
break;
|
|
272
|
-
case "start":
|
|
273
|
-
await startEngine(configPath, values.port as string, values["cp-port"] as string);
|
|
274
|
-
break;
|
|
275
|
-
default:
|
|
276
|
-
printUsage();
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
main().catch((err) => {
|
|
282
|
-
audit.error(`Unhandled execution fault: ${err.message}`);
|
|
283
|
-
process.exit(1);
|
|
284
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "./dist",
|
|
5
|
-
"rootDir": "./src"
|
|
6
|
-
},
|
|
7
|
-
"references": [
|
|
8
|
-
{ "path": "../core" },
|
|
9
|
-
{ "path": "../control-plane" }
|
|
10
|
-
],
|
|
11
|
-
"include": ["src/**/*.ts"],
|
|
12
|
-
"exclude": ["node_modules", "dist"]
|
|
13
|
-
}
|