@opensecurity/zonzon-control-plane 0.1.4 → 0.1.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.
@@ -1,9 +1,11 @@
1
- import * as http from "http";
1
+ import * as http from "node:http";
2
2
  import { ConfigService } from "./config.service.js";
3
3
  export declare class ConfigHandler {
4
4
  private service;
5
5
  private blindIndexSalt;
6
6
  private expectedApiKeyHash;
7
+ private seenNonces;
8
+ private currentPoWWindow;
7
9
  constructor(service: ConfigService, rawApiKey: string, blindIndexSalt: string);
8
10
  private readBodyStrict;
9
11
  private verifyProofOfWork;
@@ -1,40 +1,54 @@
1
- import { createHash, createHmac, timingSafeEqual } from "crypto";
1
+ import { createHash, createHmac, timingSafeEqual, hkdfSync } from "crypto";
2
2
  import { audit } from "@opensecurity/zonzon-core";
3
3
  import { ApiAuthHeaderSchema } from "./config.schema.js";
4
+ import { contextStorage } from "./context.js";
4
5
  export class ConfigHandler {
5
6
  service;
6
7
  blindIndexSalt;
7
8
  expectedApiKeyHash;
9
+ seenNonces = new Set();
10
+ currentPoWWindow = 0;
8
11
  constructor(service, rawApiKey, blindIndexSalt) {
9
12
  this.service = service;
10
13
  this.blindIndexSalt = blindIndexSalt;
11
- this.expectedApiKeyHash = createHmac("sha256", this.blindIndexSalt).update(rawApiKey).digest("hex");
14
+ const apiKeySecret = hkdfSync("sha256", this.blindIndexSalt, Buffer.alloc(0), "api_key_derivation", 32);
15
+ this.expectedApiKeyHash = createHmac("sha256", Buffer.from(apiKeySecret)).update(rawApiKey).digest("hex");
12
16
  }
13
17
  readBodyStrict(req) {
14
18
  return new Promise((resolve, reject) => {
15
- let body = "";
19
+ const chunks = [];
16
20
  let length = 0;
17
21
  req.on("data", (chunk) => {
18
22
  length += chunk.length;
19
23
  if (length > 1048576) {
24
+ req.destroy();
20
25
  reject(new Error("Payload size limit exceeded"));
21
26
  return;
22
27
  }
23
- body += chunk.toString("utf8");
28
+ chunks.push(chunk);
24
29
  });
25
- req.on("end", () => resolve(body));
30
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
26
31
  req.on("error", reject);
27
32
  });
28
33
  }
29
34
  verifyProofOfWork(nonce) {
30
- const hash = createHash("sha256").update(this.blindIndexSalt + nonce).digest("hex");
35
+ const timeWindow = Math.floor(Date.now() / 300000);
36
+ if (this.currentPoWWindow !== timeWindow) {
37
+ this.currentPoWWindow = timeWindow;
38
+ this.seenNonces.clear();
39
+ }
40
+ if (this.seenNonces.has(nonce)) {
41
+ throw new Error("Proof of Work challenge nonce already used");
42
+ }
43
+ const challenge = `${this.blindIndexSalt}:${timeWindow}`;
44
+ const hash = createHash("sha256").update(challenge + nonce).digest("hex");
31
45
  if (!hash.startsWith("0000")) {
32
46
  throw new Error("Invalid Proof of Work Challenge");
33
47
  }
48
+ this.seenNonces.add(nonce);
34
49
  }
35
50
  extractContext(req, isMutation) {
36
51
  const rawHeaders = {
37
- authorization: req.headers.authorization,
38
52
  "x-api-key": req.headers["x-api-key"],
39
53
  "x-device-id": req.headers["x-device-id"],
40
54
  "x-pow-nonce": req.headers["x-pow-nonce"],
@@ -49,7 +63,8 @@ export class ConfigHandler {
49
63
  if (!providedKey) {
50
64
  throw new Error("Missing API Key");
51
65
  }
52
- const providedHash = createHmac("sha256", this.blindIndexSalt).update(providedKey).digest("hex");
66
+ const apiKeySecret = hkdfSync("sha256", this.blindIndexSalt, Buffer.alloc(0), "api_key_derivation", 32);
67
+ const providedHash = createHmac("sha256", Buffer.from(apiKeySecret)).update(providedKey).digest("hex");
53
68
  const expectedBuffer = Buffer.from(this.expectedApiKeyHash, "utf8");
54
69
  const providedBuffer = Buffer.from(providedHash, "utf8");
55
70
  if (expectedBuffer.length !== providedBuffer.length || !timingSafeEqual(expectedBuffer, providedBuffer)) {
@@ -61,7 +76,8 @@ export class ConfigHandler {
61
76
  }
62
77
  this.verifyProofOfWork(validatedHeaders["x-pow-nonce"]);
63
78
  }
64
- const deviceHash = createHmac("sha256", this.blindIndexSalt).update(validatedHeaders["x-device-id"]).digest("hex");
79
+ const deviceSecret = hkdfSync("sha256", this.blindIndexSalt, Buffer.alloc(0), "device_id_derivation", 32);
80
+ const deviceHash = createHmac("sha256", Buffer.from(deviceSecret)).update(validatedHeaders["x-device-id"]).digest("hex");
65
81
  return {
66
82
  tenantId: "system-tenant-001",
67
83
  deviceHash: deviceHash,
@@ -71,6 +87,16 @@ export class ConfigHandler {
71
87
  const clientIp = req.socket.remoteAddress || "unknown";
72
88
  const method = req.method || "GET";
73
89
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
90
+ if (url.pathname === "/metrics") {
91
+ if (method !== "GET") {
92
+ res.writeHead(405);
93
+ res.end();
94
+ return;
95
+ }
96
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
97
+ res.end(audit.getMetricsPrometheus());
98
+ return;
99
+ }
74
100
  if (url.pathname !== "/api/v1/config") {
75
101
  res.writeHead(404, { "Content-Type": "application/json" });
76
102
  res.end(JSON.stringify({ error: "Endpoint Not Found" }));
@@ -79,24 +105,26 @@ export class ConfigHandler {
79
105
  try {
80
106
  const isMutation = method === "PUT" || method === "POST";
81
107
  const context = this.extractContext(req, isMutation);
82
- if (method === "GET") {
83
- const config = await this.service.getConfig(context);
84
- res.writeHead(200, { "Content-Type": "application/json" });
85
- res.end(JSON.stringify(config));
86
- audit.http(clientIp, "GET", "control-plane", url.pathname, 200, "Config retrieved");
87
- return;
88
- }
89
- if (method === "PUT") {
90
- const bodyStr = await this.readBodyStrict(req);
91
- const rawConfig = JSON.parse(bodyStr);
92
- await this.service.updateConfig(context, rawConfig);
93
- res.writeHead(200, { "Content-Type": "application/json" });
94
- res.end(JSON.stringify({ success: true, timestamp: Date.now() }));
95
- audit.http(clientIp, "PUT", "control-plane", url.pathname, 200, "Config updated");
96
- return;
97
- }
98
- res.writeHead(405, { "Content-Type": "application/json" });
99
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
108
+ await contextStorage.run(context, async () => {
109
+ if (method === "GET") {
110
+ const config = await this.service.getConfig();
111
+ res.writeHead(200, { "Content-Type": "application/json" });
112
+ res.end(JSON.stringify(config));
113
+ audit.http(clientIp, "GET", "control-plane", url.pathname, 200, "Config retrieved");
114
+ return;
115
+ }
116
+ if (method === "PUT") {
117
+ const bodyStr = await this.readBodyStrict(req);
118
+ const rawConfig = JSON.parse(bodyStr);
119
+ await this.service.updateConfig(rawConfig);
120
+ res.writeHead(200, { "Content-Type": "application/json" });
121
+ res.end(JSON.stringify({ success: true, timestamp: Date.now() }));
122
+ audit.http(clientIp, "PUT", "control-plane", url.pathname, 200, "Config updated");
123
+ return;
124
+ }
125
+ res.writeHead(405, { "Content-Type": "application/json" });
126
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
127
+ });
100
128
  }
101
129
  catch (error) {
102
130
  const message = error.message || "Internal Server Error";
@@ -107,7 +135,7 @@ export class ConfigHandler {
107
135
  else if (message.includes("Authentication Validation Failed") || message.includes("Unauthorized") || message.includes("API Key") || message.includes("x-pow-nonce")) {
108
136
  statusCode = 401;
109
137
  }
110
- else if (message.includes("Invalid Proof of Work")) {
138
+ else if (message.includes("Invalid Proof of Work") || message.includes("already used")) {
111
139
  statusCode = 403;
112
140
  }
113
141
  audit.error(`Control Plane API Fault: HTTP ${statusCode} | ${message} | Client: ${clientIp}`);
@@ -1,27 +1,14 @@
1
1
  import { z } from "zod";
2
- export declare const ApiAuthHeaderSchema: z.ZodEffects<z.ZodObject<{
3
- authorization: z.ZodOptional<z.ZodString>;
4
- "x-api-key": z.ZodOptional<z.ZodString>;
2
+ export declare const ApiAuthHeaderSchema: z.ZodObject<{
3
+ "x-api-key": z.ZodString;
5
4
  "x-device-id": z.ZodString;
6
5
  "x-pow-nonce": z.ZodOptional<z.ZodString>;
7
6
  }, "strip", z.ZodTypeAny, {
7
+ "x-api-key": string;
8
8
  "x-device-id": string;
9
- authorization?: string | undefined;
10
- "x-api-key"?: string | undefined;
11
9
  "x-pow-nonce"?: string | undefined;
12
10
  }, {
11
+ "x-api-key": string;
13
12
  "x-device-id": string;
14
- authorization?: string | undefined;
15
- "x-api-key"?: string | undefined;
16
- "x-pow-nonce"?: string | undefined;
17
- }>, {
18
- "x-device-id": string;
19
- authorization?: string | undefined;
20
- "x-api-key"?: string | undefined;
21
- "x-pow-nonce"?: string | undefined;
22
- }, {
23
- "x-device-id": string;
24
- authorization?: string | undefined;
25
- "x-api-key"?: string | undefined;
26
13
  "x-pow-nonce"?: string | undefined;
27
14
  }>;
@@ -1,10 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export const ApiAuthHeaderSchema = z.object({
3
- authorization: z.string().regex(/^Bearer [a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/).optional(),
4
- "x-api-key": z.string().min(32).max(128).optional(),
3
+ "x-api-key": z.string().min(32).max(128),
5
4
  "x-device-id": z.string().min(16).max(64),
6
5
  "x-pow-nonce": z.string().min(1).max(64).optional()
7
- }).refine(data => data.authorization || data["x-api-key"], {
8
- message: "Missing authentication credentials",
9
- path: ["x-api-key"]
10
6
  });
@@ -1,11 +1,11 @@
1
1
  import { ServerConfig } from "@opensecurity/zonzon-core";
2
- import { ConfigContext } from "./config.types.js";
3
2
  export declare class ConfigService {
4
3
  private config;
4
+ private configFilePath;
5
5
  private lock;
6
6
  private subscribers;
7
- constructor(initialConfig: ServerConfig);
7
+ constructor(initialConfig: ServerConfig, configFilePath: string);
8
8
  subscribe(callback: (config: ServerConfig) => void): void;
9
- getConfig(ctx: ConfigContext): Promise<ServerConfig>;
10
- updateConfig(ctx: ConfigContext, rawConfig: unknown): Promise<void>;
9
+ getConfig(): Promise<ServerConfig>;
10
+ updateConfig(rawConfig: unknown): Promise<void>;
11
11
  }
@@ -1,4 +1,7 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
1
3
  import { validateServerConfig, audit } from "@opensecurity/zonzon-core";
4
+ import { getContext } from "./context.js";
2
5
  class PessimisticLock {
3
6
  locked = false;
4
7
  queue = [];
@@ -22,27 +25,34 @@ class PessimisticLock {
22
25
  }
23
26
  export class ConfigService {
24
27
  config;
28
+ configFilePath;
25
29
  lock = new PessimisticLock();
26
30
  subscribers = [];
27
- constructor(initialConfig) {
31
+ constructor(initialConfig, configFilePath) {
28
32
  this.config = validateServerConfig(initialConfig);
33
+ this.configFilePath = path.resolve(configFilePath);
29
34
  }
30
35
  subscribe(callback) {
31
36
  this.subscribers.push(callback);
32
37
  }
33
- async getConfig(ctx) {
38
+ async getConfig() {
39
+ const ctx = getContext();
34
40
  if (!ctx.tenantId) {
35
41
  throw new Error("Security Exception: Missing Tenant Context");
36
42
  }
37
43
  return this.config;
38
44
  }
39
- async updateConfig(ctx, rawConfig) {
45
+ async updateConfig(rawConfig) {
46
+ const ctx = getContext();
40
47
  if (!ctx.tenantId) {
41
48
  throw new Error("Security Exception: Missing Tenant Context");
42
49
  }
43
50
  await this.lock.acquire();
44
51
  try {
45
52
  const validatedConfig = validateServerConfig(rawConfig);
53
+ const tempPath = `${this.configFilePath}.tmp.${Date.now()}`;
54
+ await fs.writeFile(tempPath, JSON.stringify(validatedConfig, null, 2), { mode: 0o600, encoding: "utf8" });
55
+ await fs.rename(tempPath, this.configFilePath);
46
56
  this.config = validatedConfig;
47
57
  for (const callback of this.subscribers) {
48
58
  try {
@@ -1,26 +1,34 @@
1
1
  import { describe, it, before, after } from "node:test";
2
2
  import assert from "node:assert";
3
- import * as http from "http";
3
+ import * as http from "node:http";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { promises as fs } from "node:fs";
4
7
  import { createHash } from "crypto";
5
8
  import { ConfigService } from "./config.service.js";
6
9
  import { ConfigHandler } from "./config.handler.js";
7
10
  const MOCK_SALT = "test-salt-1234567890";
8
11
  const MOCK_API_KEY = "test-api-key-very-long-and-secure-32-chars";
9
12
  const MOCK_DEVICE_ID = "test-device-id-1234";
13
+ let globalNonceCounter = 0;
10
14
  function generateValidPoW(salt) {
11
- let nonce = 0;
15
+ const timeWindow = Math.floor(Date.now() / 300000);
16
+ const challenge = `${salt}:${timeWindow}`;
12
17
  while (true) {
13
- const hash = createHash("sha256").update(salt + nonce.toString()).digest("hex");
18
+ const hash = createHash("sha256").update(challenge + globalNonceCounter.toString()).digest("hex");
14
19
  if (hash.startsWith("0000")) {
15
- return nonce.toString();
20
+ const validNonce = globalNonceCounter.toString();
21
+ globalNonceCounter++;
22
+ return validNonce;
16
23
  }
17
- nonce++;
24
+ globalNonceCounter++;
18
25
  }
19
26
  }
20
27
  describe("Control Plane API Tests", () => {
21
28
  let server;
22
29
  let port;
23
30
  let validNonce;
31
+ let tempConfigPath;
24
32
  const initialConfig = {
25
33
  port: 53,
26
34
  hosts: {
@@ -28,8 +36,10 @@ describe("Control Plane API Tests", () => {
28
36
  }
29
37
  };
30
38
  before(async () => {
39
+ tempConfigPath = path.join(os.tmpdir(), `hosts-test-${Date.now()}.json`);
40
+ await fs.writeFile(tempConfigPath, JSON.stringify(initialConfig), { mode: 0o600 });
31
41
  validNonce = generateValidPoW(MOCK_SALT);
32
- const service = new ConfigService(initialConfig);
42
+ const service = new ConfigService(initialConfig, tempConfigPath);
33
43
  const handler = new ConfigHandler(service, MOCK_API_KEY, MOCK_SALT);
34
44
  server = http.createServer((req, res) => handler.handleRequest(req, res));
35
45
  await new Promise((resolve) => {
@@ -39,8 +49,12 @@ describe("Control Plane API Tests", () => {
39
49
  });
40
50
  });
41
51
  });
42
- after(() => {
52
+ after(async () => {
43
53
  server.close();
54
+ try {
55
+ await fs.unlink(tempConfigPath);
56
+ }
57
+ catch { }
44
58
  });
45
59
  function makeRequest(method, path, headers, body) {
46
60
  return new Promise((resolve, reject) => {
@@ -63,7 +77,14 @@ describe("Control Plane API Tests", () => {
63
77
  }
64
78
  });
65
79
  });
66
- req.on("error", reject);
80
+ req.on("error", (err) => {
81
+ if (err.code === 'ECONNRESET') {
82
+ resolve({ status: 413, data: { error: "Payload too large (Socket Destroyed)" } });
83
+ }
84
+ else {
85
+ reject(err);
86
+ }
87
+ });
67
88
  if (body)
68
89
  req.write(body);
69
90
  req.end();
@@ -103,17 +124,18 @@ describe("Control Plane API Tests", () => {
103
124
  assert.strictEqual(res.status, 403);
104
125
  assert.ok(res.data.error.includes("Invalid Proof of Work"));
105
126
  });
106
- it("accepts PUT requests with valid configuration and PoW", async () => {
127
+ it("accepts PUT requests with valid configuration and PoW, and writes to disk", async () => {
107
128
  const newConfig = {
108
129
  port: 5353,
109
130
  hosts: {
110
131
  "updated.loop": { records: [{ type: "A", address: "8.8.8.8" }] }
111
132
  }
112
133
  };
134
+ const freshNonce = generateValidPoW(MOCK_SALT);
113
135
  const res = await makeRequest("PUT", "/api/v1/config", {
114
136
  "x-api-key": MOCK_API_KEY,
115
137
  "x-device-id": MOCK_DEVICE_ID,
116
- "x-pow-nonce": validNonce,
138
+ "x-pow-nonce": freshNonce,
117
139
  "Content-Type": "application/json"
118
140
  }, JSON.stringify(newConfig));
119
141
  assert.strictEqual(res.status, 200);
@@ -125,13 +147,18 @@ describe("Control Plane API Tests", () => {
125
147
  assert.strictEqual(checkRes.status, 200);
126
148
  assert.strictEqual(checkRes.data.port, 5353);
127
149
  assert.ok("updated.loop" in checkRes.data.hosts);
150
+ const diskContent = await fs.readFile(tempConfigPath, "utf8");
151
+ const parsedDisk = JSON.parse(diskContent);
152
+ assert.strictEqual(parsedDisk.port, 5353);
153
+ assert.ok("updated.loop" in parsedDisk.hosts);
128
154
  });
129
155
  it("rejects payloads exceeding the memory boundary", async () => {
130
156
  const hugePayload = JSON.stringify({ port: 53, hosts: {} }) + " ".repeat(1.5 * 1024 * 1024);
157
+ const boundaryNonce = generateValidPoW(MOCK_SALT);
131
158
  const res = await makeRequest("PUT", "/api/v1/config", {
132
159
  "x-api-key": MOCK_API_KEY,
133
160
  "x-device-id": MOCK_DEVICE_ID,
134
- "x-pow-nonce": validNonce,
161
+ "x-pow-nonce": boundaryNonce,
135
162
  "Content-Type": "application/json"
136
163
  }, hugePayload);
137
164
  assert.strictEqual(res.status, 413);
@@ -0,0 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { ConfigContext } from "./config.types.js";
3
+ export declare const contextStorage: AsyncLocalStorage<ConfigContext>;
4
+ export declare function getContext(): ConfigContext;
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const contextStorage = new AsyncLocalStorage();
3
+ export function getContext() {
4
+ const store = contextStorage.getStore();
5
+ if (!store) {
6
+ throw new Error("Security Exception: Context missing. Ensure request is wrapped in middleware.");
7
+ }
8
+ return store;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { getContext, contextStorage } from "./context.js";
4
+ describe("AsyncLocalStorage Context Boundary", () => {
5
+ it("throws error when accessing context outside of storage run", () => {
6
+ assert.throws(() => getContext(), /Security Exception: Context missing/);
7
+ });
8
+ it("successfully retrieves injected context payload", () => {
9
+ const mockContext = {
10
+ tenantId: "test-tenant-001",
11
+ deviceHash: "deadbeef"
12
+ };
13
+ contextStorage.run(mockContext, () => {
14
+ const ctx = getContext();
15
+ assert.strictEqual(ctx.tenantId, "test-tenant-001");
16
+ assert.strictEqual(ctx.deviceHash, "deadbeef");
17
+ });
18
+ });
19
+ it("maintains isolation across asynchronous boundaries", async () => {
20
+ const contextA = { tenantId: "tenant-A", deviceHash: "hash-A" };
21
+ const contextB = { tenantId: "tenant-B", deviceHash: "hash-B" };
22
+ const promiseA = contextStorage.run(contextA, async () => {
23
+ await new Promise(resolve => setTimeout(resolve, 10));
24
+ const ctx = getContext();
25
+ assert.strictEqual(ctx.tenantId, "tenant-A");
26
+ });
27
+ const promiseB = contextStorage.run(contextB, async () => {
28
+ await new Promise(resolve => setTimeout(resolve, 5));
29
+ const ctx = getContext();
30
+ assert.strictEqual(ctx.tenantId, "tenant-B");
31
+ });
32
+ await Promise.all([promiseA, promiseB]);
33
+ });
34
+ });
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { ServerConfig } from "@opensecurity/zonzon-core";
2
2
  export interface ControlPlaneOptions {
3
- port: number;
3
+ port?: number;
4
+ socketPath?: string;
4
5
  apiKey: string;
5
6
  blindIndexSalt: string;
6
7
  initialConfig: ServerConfig;
8
+ configFilePath: string;
7
9
  }
8
10
  export declare class ControlPlane {
9
11
  private service;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import * as http from "http";
1
+ import * as http from "node:http";
2
+ import * as fs from "node:fs";
2
3
  import { audit } from "@opensecurity/zonzon-core";
3
4
  import { ConfigService } from "./domain/config/config.service.js";
4
5
  import { ConfigHandler } from "./domain/config/config.handler.js";
@@ -8,7 +9,7 @@ export class ControlPlane {
8
9
  options;
9
10
  constructor(options) {
10
11
  this.options = options;
11
- this.service = new ConfigService(options.initialConfig);
12
+ this.service = new ConfigService(options.initialConfig, options.configFilePath);
12
13
  }
13
14
  subscribe(callback) {
14
15
  this.service.subscribe(callback);
@@ -26,10 +27,22 @@ export class ControlPlane {
26
27
  });
27
28
  });
28
29
  this.server.on("error", reject);
29
- this.server.listen(this.options.port, "127.0.0.1", () => {
30
- audit.system(`Control Plane locked strictly to loopback interface on port ${this.options.port}`);
31
- resolve();
32
- });
30
+ if (this.options.socketPath) {
31
+ if (fs.existsSync(this.options.socketPath)) {
32
+ fs.unlinkSync(this.options.socketPath);
33
+ }
34
+ this.server.listen(this.options.socketPath, () => {
35
+ fs.chmodSync(this.options.socketPath, 0o600);
36
+ audit.system(`Control Plane locked strictly to Unix Domain Socket at ${this.options.socketPath}`);
37
+ resolve();
38
+ });
39
+ }
40
+ else {
41
+ this.server.listen(this.options.port || 8080, "127.0.0.1", () => {
42
+ audit.system(`Control Plane locked strictly to loopback interface on port ${this.options.port || 8080}`);
43
+ resolve();
44
+ });
45
+ }
33
46
  });
34
47
  }
35
48
  async stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensecurity/zonzon-control-plane",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "dynamic configuration api for the core engine",
5
5
  "type": "module",
6
6
  "author": "Lucian BLETAN <neuraluc@gmail.com>",
@@ -28,7 +28,7 @@
28
28
  "build": "tsc -b"
29
29
  },
30
30
  "dependencies": {
31
- "@opensecurity/zonzon-core": "^0.1.4",
31
+ "@opensecurity/zonzon-core": "^0.1.6",
32
32
  "zod": "^3.24.2"
33
33
  }
34
- }
34
+ }