@ao_zorin/zocket 1.1.0 → 1.3.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/zocket.js CHANGED
@@ -3,11 +3,12 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import { createServer } from "http";
6
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
7
- import { dirname as dirname5 } from "path";
8
- import { randomBytes as randomBytes6 } from "crypto";
6
+ import { mkdirSync as mkdirSync5 } from "fs";
7
+ import { randomUUID } from "crypto";
9
8
  import { serve } from "@hono/node-server";
10
9
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
11
12
 
12
13
  // src/vault.ts
13
14
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -15,11 +16,26 @@ import { dirname } from "path";
15
16
  import { lock } from "proper-lockfile";
16
17
 
17
18
  // src/crypto.ts
18
- import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
19
+ import { randomBytes, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from "crypto";
19
20
  var VERSION = 1;
20
21
  var ALGORITHM = "aes-256-gcm";
21
22
  var IV_BYTES = 12;
22
23
  var TAG_BYTES = 16;
24
+ var FERNET_VERSION = 128;
25
+ function base64UrlToBuffer(raw) {
26
+ const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
27
+ const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
28
+ return Buffer.from(normalized + pad, "base64");
29
+ }
30
+ function pkcs7Unpad(buf) {
31
+ if (!buf.length) throw new Error("Invalid PKCS7 padding");
32
+ const pad = buf[buf.length - 1];
33
+ if (pad < 1 || pad > 16) throw new Error("Invalid PKCS7 padding");
34
+ for (let i = buf.length - pad; i < buf.length; i += 1) {
35
+ if (buf[i] !== pad) throw new Error("Invalid PKCS7 padding");
36
+ }
37
+ return buf.subarray(0, buf.length - pad);
38
+ }
23
39
  function encrypt(plaintext, key) {
24
40
  const iv = randomBytes(IV_BYTES);
25
41
  const cipher = createCipheriv(ALGORITHM, key, iv);
@@ -39,25 +55,86 @@ function decrypt(data, key) {
39
55
  decipher.setAuthTag(tag);
40
56
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
41
57
  }
58
+ function decryptLegacyFernet(tokenData, keyBase64) {
59
+ const token = tokenData.toString("utf8").trim();
60
+ const raw = base64UrlToBuffer(token);
61
+ if (raw.length < 1 + 8 + 16 + 32) throw new Error("Invalid legacy token length");
62
+ if (raw[0] !== FERNET_VERSION) throw new Error("Invalid legacy token version");
63
+ const key = base64UrlToBuffer(keyBase64);
64
+ if (key.length !== 32) throw new Error("Invalid legacy key length");
65
+ const signingKey = key.subarray(0, 16);
66
+ const encryptionKey = key.subarray(16, 32);
67
+ const hmacStart = raw.length - 32;
68
+ const msg = raw.subarray(0, hmacStart);
69
+ const sig = raw.subarray(hmacStart);
70
+ const mac = createHmac("sha256", signingKey).update(msg).digest();
71
+ if (mac.length !== sig.length || !timingSafeEqual(mac, sig)) {
72
+ throw new Error("Legacy token HMAC verification failed");
73
+ }
74
+ const iv = raw.subarray(1 + 8, 1 + 8 + 16);
75
+ const ciphertext = raw.subarray(1 + 8 + 16, hmacStart);
76
+ const decipher = createDecipheriv("aes-128-cbc", encryptionKey, iv);
77
+ decipher.setAutoPadding(false);
78
+ const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
79
+ return pkcs7Unpad(padded);
80
+ }
42
81
 
43
82
  // src/vault.ts
44
83
  var PROJECT_RE = /^[a-zA-Z0-9._-]+$/;
45
84
  var SECRET_RE = /^[A-Z_][A-Z0-9_]*$/;
46
85
  var VaultService = class {
47
- constructor(vaultPath2, lockFile, key) {
86
+ constructor(vaultPath2, lockFile, key, legacyKeyBase64, keyFilePath) {
48
87
  this.vaultPath = vaultPath2;
49
88
  this.lockFile = lockFile;
50
89
  this.key = key;
90
+ this.legacyKeyBase64 = legacyKeyBase64;
91
+ this.keyFilePath = keyFilePath;
51
92
  }
52
93
  load() {
53
94
  if (!existsSync(this.vaultPath)) return { version: 1, projects: {} };
54
95
  const raw = readFileSync(this.vaultPath);
55
- return JSON.parse(decrypt(raw, this.key).toString("utf8"));
96
+ try {
97
+ return JSON.parse(decrypt(raw, this.key).toString("utf8"));
98
+ } catch (err2) {
99
+ const message = String(err2);
100
+ if (this.legacyKeyBase64 && message.includes("Unsupported vault version")) {
101
+ return this.migrateLegacy(raw);
102
+ }
103
+ throw err2;
104
+ }
56
105
  }
57
106
  save(data) {
58
107
  mkdirSync(dirname(this.vaultPath), { recursive: true });
59
108
  writeFileSync(this.vaultPath, encrypt(Buffer.from(JSON.stringify(data)), this.key));
60
109
  }
110
+ ensureExists() {
111
+ if (!existsSync(this.vaultPath)) {
112
+ this.save({ version: 1, projects: {} });
113
+ }
114
+ }
115
+ migrateLegacy(raw) {
116
+ const plaintext = decryptLegacyFernet(raw, this.legacyKeyBase64);
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(plaintext.toString("utf8"));
120
+ } catch {
121
+ throw new Error("Legacy vault JSON parse failed");
122
+ }
123
+ const data = {
124
+ version: typeof parsed.version === "number" ? parsed.version : 1,
125
+ projects: parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {}
126
+ };
127
+ if (!data.version || data.version !== 1) data.version = 1;
128
+ if (!data.projects) data.projects = {};
129
+ this.save(data);
130
+ if (this.keyFilePath) {
131
+ try {
132
+ writeFileSync(this.keyFilePath, this.key.toString("hex"), { mode: 384 });
133
+ } catch {
134
+ }
135
+ }
136
+ return data;
137
+ }
61
138
  async withLock(fn) {
62
139
  mkdirSync(dirname(this.lockFile), { recursive: true });
63
140
  if (!existsSync(this.lockFile)) writeFileSync(this.lockFile, "");
@@ -302,12 +379,12 @@ function runCommand(command, env, policy, maxChars = 500) {
302
379
  r.exit_code = proc.status ?? 1;
303
380
  return r;
304
381
  }
305
- function runScript(lang, code, env, maxChars = 500) {
306
- const ext = lang === "node" ? ".mjs" : ".py";
382
+ function runScript(_lang, code, env, maxChars = 500) {
383
+ const ext = ".mjs";
307
384
  const tmpFile = join(tmpdir(), `zkt-${randomBytes3(8).toString("hex")}${ext}`);
308
385
  try {
309
386
  writeFileSync3(tmpFile, code, "utf8");
310
- const bin = lang === "node" ? "node" : "python3";
387
+ const bin = "node";
311
388
  const proc = spawnSync(bin, [tmpFile], {
312
389
  env: { ...process.env, ...env },
313
390
  encoding: "utf8",
@@ -483,8 +560,8 @@ function envRefsIn(text) {
483
560
  const matches = [...text.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map((m) => m[1]);
484
561
  return [...new Set(matches)];
485
562
  }
486
- function buildCtx(text, lang) {
487
- return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text), lang };
563
+ function buildCtx(text) {
564
+ return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text) };
488
565
  }
489
566
  var RULES = [
490
567
  // ── Critical ────────────────────────────────────────────────────────────
@@ -577,13 +654,6 @@ var RULES = [
577
654
  weight: W.MEDIUM,
578
655
  test: (c) => /\b(printenv|env)\b[^|]*>/.test(c.text)
579
656
  },
580
- {
581
- id: "PYTHON_SUBPROCESS_EXFIL",
582
- description: "Python subprocess call invoking network tool (curl/wget/nc)",
583
- severity: "medium",
584
- weight: W.MEDIUM,
585
- test: (c) => c.lang === "python" && /subprocess\.(run|Popen|call|check_output)\([^)]*\b(curl|wget|nc\b|netcat)\b/.test(c.text)
586
- },
587
657
  // ── Low ──────────────────────────────────────────────────────────────────
588
658
  {
589
659
  id: "SINGLE_ENV_NETWORK",
@@ -648,9 +718,9 @@ var SecurityAnalyzer = class {
648
718
  const text = isBashC ? command[2] : command.join(" ");
649
719
  return this.analyze(buildCtx(text));
650
720
  }
651
- analyzeScript(lang, code) {
721
+ analyzeScript(_lang, code) {
652
722
  if (this.cfg.mode === "off") return allow();
653
- const ctx = buildCtx(code, lang);
723
+ const ctx = buildCtx(code);
654
724
  return this.analyze(ctx);
655
725
  }
656
726
  analyze(ctx) {
@@ -870,18 +940,18 @@ function buildCatalog(services) {
870
940
  },
871
941
  {
872
942
  name: "run_script",
873
- summary: "Run an inline node/python script with project secrets injected as env vars. Prefer over multiple run_with_project_env calls.",
943
+ summary: "Run an inline node script with project secrets injected as env vars. Prefer over multiple run_with_project_env calls.",
874
944
  register: (server) => {
875
945
  server.tool(
876
946
  "run_script",
877
947
  [
878
- "Run an inline script with project secrets available as environment variables.",
948
+ "Run an inline node script with project secrets available as environment variables.",
879
949
  "Use this instead of multiple run_with_project_env calls \u2014 write the full logic in one script.",
880
950
  "Filesystem is NOT shared between calls. Secret values never appear in this conversation."
881
951
  ].join(" "),
882
952
  {
883
953
  project: z.string().describe("Project name"),
884
- lang: z.enum(["node", "python"]).describe("Script language"),
954
+ lang: z.enum(["node"]).describe("Script language"),
885
955
  code: z.string().min(1).describe("Full script source code"),
886
956
  max_chars: z.number().int().min(1).max(32e3).optional().describe("Max output chars (default ~500)"),
887
957
  confirm: z.boolean().optional().describe("Set to true to confirm execution of a medium-risk script after reviewing the warning")
@@ -1118,12 +1188,12 @@ function createMcpServer(services, options = {}) {
1118
1188
  // src/web.ts
1119
1189
  import { Hono } from "hono";
1120
1190
  import { getCookie, setCookie, deleteCookie } from "hono/cookie";
1121
- import { createHmac, randomBytes as randomBytes5 } from "crypto";
1191
+ import { createHmac as createHmac2, randomBytes as randomBytes5 } from "crypto";
1122
1192
  import { readdirSync, statSync, existsSync as existsSync4 } from "fs";
1123
1193
  import { join as join2, resolve as resolvePath, dirname as dirname4 } from "path";
1124
1194
 
1125
1195
  // src/auth.ts
1126
- import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual } from "crypto";
1196
+ import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual as timingSafeEqual2 } from "crypto";
1127
1197
  var ITERATIONS = 6e5;
1128
1198
  var KEY_LEN = 32;
1129
1199
  var DIGEST = "sha256";
@@ -1136,7 +1206,7 @@ function verifyPassword(password, hash, salt) {
1136
1206
  const derived = pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST);
1137
1207
  const expected = Buffer.from(hash, "hex");
1138
1208
  if (derived.length !== expected.length) return false;
1139
- return timingSafeEqual(derived, expected);
1209
+ return timingSafeEqual2(derived, expected);
1140
1210
  }
1141
1211
 
1142
1212
  // src/i18n.ts
@@ -1338,7 +1408,7 @@ function normalizeLang(lang) {
1338
1408
  var COOKIE = "zs";
1339
1409
  function signSession(data, secret) {
1340
1410
  const payload = Buffer.from(JSON.stringify(data)).toString("base64url");
1341
- const sig = createHmac("sha256", secret).update(payload).digest("hex").slice(0, 20);
1411
+ const sig = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
1342
1412
  return `${payload}.${sig}`;
1343
1413
  }
1344
1414
  function parseSession(value, secret) {
@@ -1346,7 +1416,7 @@ function parseSession(value, secret) {
1346
1416
  if (dot === -1) return null;
1347
1417
  const payload = value.slice(0, dot);
1348
1418
  const sig = value.slice(dot + 1);
1349
- const expected = createHmac("sha256", secret).update(payload).digest("hex").slice(0, 20);
1419
+ const expected = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
1350
1420
  if (sig !== expected) return null;
1351
1421
  try {
1352
1422
  return JSON.parse(Buffer.from(payload, "base64url").toString());
@@ -1922,31 +1992,383 @@ function lockPath(home = zocketHome()) {
1922
1992
  return join3(home, "vault.lock");
1923
1993
  }
1924
1994
 
1925
- // src/cli.ts
1995
+ // src/keys.ts
1996
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
1997
+ import { dirname as dirname5 } from "path";
1998
+ import { randomBytes as randomBytes6 } from "crypto";
1999
+ function isHexKey(raw) {
2000
+ return /^[0-9a-fA-F]{64}$/.test(raw);
2001
+ }
2002
+ function isBase64UrlKey(raw) {
2003
+ return /^[A-Za-z0-9_-]{43,44}=?$/.test(raw);
2004
+ }
2005
+ function base64UrlToBuffer2(raw) {
2006
+ const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
2007
+ const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
2008
+ return Buffer.from(normalized + pad, "base64");
2009
+ }
1926
2010
  function loadOrCreateKey(keyFile) {
1927
2011
  if (existsSync5(keyFile)) {
1928
2012
  const raw = readFileSync5(keyFile, "utf8").trim();
1929
- return Buffer.from(raw, "hex");
2013
+ if (isHexKey(raw)) {
2014
+ return { key: Buffer.from(raw, "hex"), source: "hex" };
2015
+ }
2016
+ if (isBase64UrlKey(raw)) {
2017
+ const key2 = base64UrlToBuffer2(raw);
2018
+ if (key2.length !== 32) {
2019
+ throw new Error("Legacy base64 key is invalid length");
2020
+ }
2021
+ return { key: key2, source: "base64", legacyBase64: raw };
2022
+ }
2023
+ throw new Error("Unsupported key format: expected 64-char hex or base64url");
1930
2024
  }
1931
2025
  mkdirSync4(dirname5(keyFile), { recursive: true });
1932
2026
  const key = randomBytes6(32);
1933
2027
  writeFileSync5(keyFile, key.toString("hex"), { mode: 384 });
1934
- return key;
2028
+ return { key, source: "generated" };
1935
2029
  }
1936
- async function cmdStart(opts) {
2030
+
2031
+ // src/tui.ts
2032
+ import blessed from "blessed";
2033
+ function safeText(value) {
2034
+ if (!value) return "";
2035
+ return String(value);
2036
+ }
2037
+ function buildProjectLabel(row) {
2038
+ const count = row.secret_count ?? 0;
2039
+ return `${row.name} (${count})`;
2040
+ }
2041
+ async function runTui(services) {
2042
+ const { vault } = services;
2043
+ const screen = blessed.screen({
2044
+ smartCSR: true,
2045
+ title: "zocket"
2046
+ });
2047
+ const header = blessed.box({
2048
+ parent: screen,
2049
+ top: 0,
2050
+ height: 1,
2051
+ width: "100%",
2052
+ style: { fg: "white", bg: "blue" },
2053
+ content: " zocket \u2022 TUI | q:quit tab:switch r:refresh v:values n:new project d:delete s:set secret e:edit x:delete"
2054
+ });
2055
+ const footer = blessed.box({
2056
+ parent: screen,
2057
+ bottom: 0,
2058
+ height: 1,
2059
+ width: "100%",
2060
+ style: { fg: "black", bg: "white" },
2061
+ content: " Ready"
2062
+ });
2063
+ const projectList = blessed.list({
2064
+ parent: screen,
2065
+ label: " Projects ",
2066
+ top: 1,
2067
+ left: 0,
2068
+ bottom: 1,
2069
+ width: "30%",
2070
+ border: "line",
2071
+ keys: true,
2072
+ mouse: true,
2073
+ style: {
2074
+ selected: { bg: "blue", fg: "white" },
2075
+ border: { fg: "cyan" }
2076
+ },
2077
+ scrollbar: {
2078
+ ch: " ",
2079
+ inverse: true
2080
+ }
2081
+ });
2082
+ const secretTable = blessed.listtable({
2083
+ parent: screen,
2084
+ label: " Secrets ",
2085
+ top: 1,
2086
+ left: "30%",
2087
+ bottom: 1,
2088
+ width: "70%",
2089
+ border: "line",
2090
+ keys: true,
2091
+ mouse: true,
2092
+ align: "left",
2093
+ noCellBorders: true,
2094
+ style: {
2095
+ header: { fg: "yellow", bold: true },
2096
+ cell: { fg: "white" },
2097
+ selected: { bg: "blue", fg: "white" },
2098
+ border: { fg: "cyan" }
2099
+ }
2100
+ });
2101
+ const prompt = blessed.prompt({
2102
+ parent: screen,
2103
+ border: "line",
2104
+ height: 9,
2105
+ width: "60%",
2106
+ top: "center",
2107
+ left: "center",
2108
+ label: " Input ",
2109
+ keys: true,
2110
+ vi: true
2111
+ });
2112
+ const confirm = blessed.question({
2113
+ parent: screen,
2114
+ border: "line",
2115
+ height: 7,
2116
+ width: "60%",
2117
+ top: "center",
2118
+ left: "center",
2119
+ label: " Confirm ",
2120
+ keys: true,
2121
+ vi: true
2122
+ });
2123
+ let projects = [];
2124
+ let secrets = [];
2125
+ let currentProject = null;
2126
+ let showValues = false;
2127
+ function setStatus(message) {
2128
+ footer.setContent(` ${message}`);
2129
+ screen.render();
2130
+ }
2131
+ function setFocusPane(pane) {
2132
+ if (pane === "projects") {
2133
+ projectList.style.border.fg = "green";
2134
+ secretTable.style.border.fg = "cyan";
2135
+ projectList.setLabel(" Projects ");
2136
+ secretTable.setLabel(" Secrets ");
2137
+ projectList.focus();
2138
+ } else {
2139
+ projectList.style.border.fg = "cyan";
2140
+ secretTable.style.border.fg = "green";
2141
+ projectList.setLabel(" Projects ");
2142
+ secretTable.setLabel(" Secrets ");
2143
+ secretTable.focus();
2144
+ }
2145
+ screen.render();
2146
+ }
2147
+ async function askInput(label, initial = "") {
2148
+ return await new Promise((resolve) => {
2149
+ prompt.input(label, initial, (err2, value) => {
2150
+ if (err2) return resolve(null);
2151
+ resolve(value ?? "");
2152
+ });
2153
+ });
2154
+ }
2155
+ async function askConfirm(label) {
2156
+ return await new Promise((resolve) => {
2157
+ confirm.ask(label, (err2, answer) => {
2158
+ if (err2) return resolve(false);
2159
+ resolve(Boolean(answer));
2160
+ });
2161
+ });
2162
+ }
2163
+ async function refreshProjects(selectName) {
2164
+ projects = await vault.listProjects();
2165
+ if (!projects.length) {
2166
+ projectList.setItems(["(no projects)"]);
2167
+ projectList.select(0);
2168
+ currentProject = null;
2169
+ return;
2170
+ }
2171
+ const labels = projects.map(buildProjectLabel);
2172
+ projectList.setItems(labels);
2173
+ let idx = 0;
2174
+ if (selectName) {
2175
+ const found = projects.findIndex((p) => p.name === selectName);
2176
+ if (found >= 0) idx = found;
2177
+ } else if (currentProject) {
2178
+ const found = projects.findIndex((p) => p.name === currentProject?.name);
2179
+ if (found >= 0) idx = found;
2180
+ }
2181
+ projectList.select(idx);
2182
+ currentProject = projects[idx];
2183
+ }
2184
+ async function refreshSecrets() {
2185
+ if (!currentProject) {
2186
+ secretTable.setData([["Key", "Value", "Description", "Updated"]]);
2187
+ return;
2188
+ }
2189
+ secrets = await vault.listSecrets(currentProject.name);
2190
+ let rows;
2191
+ if (!secrets.length) {
2192
+ rows = [[showValues ? "Key" : "Key", showValues ? "Value" : "Description", "Updated"]];
2193
+ } else {
2194
+ if (showValues) {
2195
+ const values = await Promise.all(secrets.map((s) => vault.getSecretValue(currentProject.name, s.key)));
2196
+ rows = [
2197
+ ["Key", "Value", "Description", "Updated"],
2198
+ ...secrets.map((s, i) => [
2199
+ s.key,
2200
+ safeText(values[i]),
2201
+ safeText(s.description),
2202
+ safeText(s.updated_at)
2203
+ ])
2204
+ ];
2205
+ } else {
2206
+ rows = [
2207
+ ["Key", "Description", "Updated"],
2208
+ ...secrets.map((s) => [s.key, safeText(s.description), safeText(s.updated_at)])
2209
+ ];
2210
+ }
2211
+ }
2212
+ secretTable.setData(rows);
2213
+ }
2214
+ async function refreshAll() {
2215
+ await refreshProjects();
2216
+ await refreshSecrets();
2217
+ screen.render();
2218
+ }
2219
+ async function createProject() {
2220
+ const name = await askInput("Project name");
2221
+ if (!name) return;
2222
+ const desc = await askInput("Description (optional)", "");
2223
+ const folder = await askInput("Folder path (optional)", "");
2224
+ await vault.createProject(name, desc ?? "");
2225
+ if (folder) await vault.setFolder(name, folder);
2226
+ setStatus(`Project created: ${name}`);
2227
+ await refreshProjects(name);
2228
+ await refreshSecrets();
2229
+ }
2230
+ async function deleteProject() {
2231
+ if (!currentProject) return;
2232
+ const ok2 = await askConfirm(`Delete project ${currentProject.name}?`);
2233
+ if (!ok2) return;
2234
+ await vault.deleteProject(currentProject.name);
2235
+ setStatus(`Project deleted: ${currentProject.name}`);
2236
+ await refreshProjects();
2237
+ await refreshSecrets();
2238
+ }
2239
+ async function setProjectFolder() {
2240
+ if (!currentProject) return;
2241
+ const path = await askInput("Folder path (empty to clear)", currentProject.folder_path ?? "");
2242
+ if (path === null) return;
2243
+ await vault.setFolder(currentProject.name, path.trim() || void 0);
2244
+ setStatus("Folder updated");
2245
+ await refreshProjects(currentProject.name);
2246
+ }
2247
+ async function setAllowedDomains() {
2248
+ if (!currentProject) return;
2249
+ const initial = (await vault.getAllowedDomains(currentProject.name))?.join(", ") ?? "";
2250
+ const domains = await askInput("Allowed domains (comma-separated)", initial);
2251
+ if (domains === null) return;
2252
+ const value = domains.split(",").map((s) => s.trim()).filter(Boolean);
2253
+ await vault.setAllowedDomains(currentProject.name, value.length ? value : null);
2254
+ setStatus("Allowed domains updated");
2255
+ }
2256
+ function selectedSecretKey() {
2257
+ if (!secrets.length) return null;
2258
+ const idx = secretTable.selected - 1;
2259
+ if (idx < 0 || idx >= secrets.length) return null;
2260
+ return secrets[idx].key;
2261
+ }
2262
+ async function upsertSecret(edit = false) {
2263
+ if (!currentProject) return;
2264
+ let key = "";
2265
+ let value = "";
2266
+ let desc = "";
2267
+ if (edit) {
2268
+ const selectedKey = selectedSecretKey();
2269
+ if (!selectedKey) return;
2270
+ key = selectedKey;
2271
+ desc = secrets.find((s) => s.key === selectedKey)?.description ?? "";
2272
+ value = showValues ? await vault.getSecretValue(currentProject.name, selectedKey) : "";
2273
+ }
2274
+ const keyInput = await askInput("Key (UPPERCASE)", key);
2275
+ if (!keyInput) return;
2276
+ const valueInput = await askInput("Value", value);
2277
+ if (valueInput === null) return;
2278
+ const descInput = await askInput("Description (optional)", desc);
2279
+ await vault.setSecret(currentProject.name, keyInput, valueInput, descInput ?? "");
2280
+ setStatus("Secret saved");
2281
+ await refreshSecrets();
2282
+ }
2283
+ async function deleteSecret() {
2284
+ if (!currentProject) return;
2285
+ const key = selectedSecretKey();
2286
+ if (!key) return;
2287
+ const ok2 = await askConfirm(`Delete secret ${key}?`);
2288
+ if (!ok2) return;
2289
+ await vault.deleteSecret(currentProject.name, key);
2290
+ setStatus(`Secret deleted: ${key}`);
2291
+ await refreshSecrets();
2292
+ }
2293
+ projectList.on("select", async (_, idx) => {
2294
+ currentProject = projects[idx] ?? null;
2295
+ await refreshSecrets();
2296
+ screen.render();
2297
+ });
2298
+ screen.key(["q", "C-c"], () => process.exit(0));
2299
+ screen.key(["tab"], () => {
2300
+ if (screen.focused === projectList) setFocusPane("secrets");
2301
+ else setFocusPane("projects");
2302
+ });
2303
+ screen.key(["r"], async () => {
2304
+ setStatus("Refreshing...");
2305
+ await refreshAll();
2306
+ setStatus("Ready");
2307
+ });
2308
+ screen.key(["v"], async () => {
2309
+ showValues = !showValues;
2310
+ setStatus(showValues ? "Values: visible" : "Values: hidden");
2311
+ await refreshSecrets();
2312
+ screen.render();
2313
+ });
2314
+ screen.key(["n"], async () => {
2315
+ await createProject();
2316
+ screen.render();
2317
+ });
2318
+ screen.key(["d"], async () => {
2319
+ await deleteProject();
2320
+ screen.render();
2321
+ });
2322
+ screen.key(["f"], async () => {
2323
+ await setProjectFolder();
2324
+ screen.render();
2325
+ });
2326
+ screen.key(["a"], async () => {
2327
+ await setAllowedDomains();
2328
+ screen.render();
2329
+ });
2330
+ screen.key(["s"], async () => {
2331
+ await upsertSecret(false);
2332
+ screen.render();
2333
+ });
2334
+ screen.key(["e"], async () => {
2335
+ await upsertSecret(true);
2336
+ screen.render();
2337
+ });
2338
+ screen.key(["x"], async () => {
2339
+ await deleteSecret();
2340
+ screen.render();
2341
+ });
2342
+ await refreshAll();
2343
+ setFocusPane("projects");
2344
+ screen.render();
2345
+ }
2346
+
2347
+ // src/cli.ts
2348
+ function createServices() {
1937
2349
  const home = zocketHome();
1938
- mkdirSync4(home, { recursive: true });
1939
- const key = loadOrCreateKey(keyPath());
1940
- const vault = new VaultService(vaultPath(), lockPath(), key);
2350
+ mkdirSync5(home, { recursive: true });
2351
+ const keyMaterial = loadOrCreateKey(keyPath());
2352
+ const vault = new VaultService(vaultPath(), lockPath(), keyMaterial.key, keyMaterial.legacyBase64, keyPath());
2353
+ vault.ensureExists();
1941
2354
  const config = new ConfigStore(configPath());
1942
2355
  const audit = new AuditLogger(auditPath());
2356
+ config.ensureExists();
2357
+ return { vault, config, audit };
2358
+ }
2359
+ async function cmdStart(opts) {
2360
+ const { vault, config, audit } = createServices();
1943
2361
  const cfg = config.ensureExists();
1944
2362
  const mode = opts.mode === "admin" ? "admin" : "metadata";
1945
2363
  const services = { vault, config, audit, mode };
1946
- const webApp = createWebApp({ vault, config, audit });
1947
- serve({ fetch: webApp.fetch, hostname: opts.host, port: opts.webPort }, () => {
1948
- console.log(`[zocket] web http://${opts.host}:${opts.webPort}`);
1949
- });
2364
+ if (opts.webEnabled) {
2365
+ const webApp = createWebApp({ vault, config, audit });
2366
+ serve({ fetch: webApp.fetch, hostname: opts.host, port: opts.webPort }, () => {
2367
+ console.log(`[zocket] web http://${opts.host}:${opts.webPort}`);
2368
+ });
2369
+ } else {
2370
+ console.log("[zocket] web disabled");
2371
+ }
1950
2372
  const sessions = /* @__PURE__ */ new Map();
1951
2373
  const mcpHttp = createServer((req, res) => {
1952
2374
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -1976,19 +2398,180 @@ async function cmdStart(opts) {
1976
2398
  });
1977
2399
  mcpHttp.listen(opts.mcpPort, opts.host, () => {
1978
2400
  console.log(`[zocket] mcp http://${opts.host}:${opts.mcpPort}/sse (mode: ${mode}, loading: ${cfg.mcp_loading})`);
2401
+ });
2402
+ const streamableSessions = /* @__PURE__ */ new Map();
2403
+ const streamableHttp = createServer(async (req, res) => {
2404
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
2405
+ if (url.pathname !== "/mcp") {
2406
+ res.writeHead(404).end("Not found");
2407
+ return;
2408
+ }
2409
+ const method = (req.method ?? "GET").toUpperCase();
2410
+ let parsedBody = void 0;
2411
+ if (method === "POST") {
2412
+ let raw = "";
2413
+ for await (const chunk of req) {
2414
+ raw += chunk;
2415
+ }
2416
+ if (raw.trim().length > 0) {
2417
+ try {
2418
+ parsedBody = JSON.parse(raw);
2419
+ } catch {
2420
+ res.writeHead(400, { "Content-Type": "application/json" });
2421
+ res.end(JSON.stringify({
2422
+ jsonrpc: "2.0",
2423
+ error: { code: -32700, message: "Parse error" },
2424
+ id: null
2425
+ }));
2426
+ return;
2427
+ }
2428
+ }
2429
+ }
2430
+ const header = req.headers["mcp-session-id"];
2431
+ const sessionId = Array.isArray(header) ? header[0] : header;
2432
+ let transport;
2433
+ if (sessionId && streamableSessions.has(sessionId)) {
2434
+ transport = streamableSessions.get(sessionId);
2435
+ } else if (!sessionId && method === "POST" && parsedBody && isInitializeRequest(parsedBody)) {
2436
+ transport = new StreamableHTTPServerTransport({
2437
+ sessionIdGenerator: () => randomUUID(),
2438
+ onsessioninitialized: (sid) => {
2439
+ if (transport) streamableSessions.set(sid, transport);
2440
+ }
2441
+ });
2442
+ transport.onclose = () => {
2443
+ const sid = transport?.sessionId;
2444
+ if (sid && streamableSessions.has(sid)) streamableSessions.delete(sid);
2445
+ };
2446
+ const mcpServer = createMcpServer(services, { loading: cfg.mcp_loading });
2447
+ mcpServer.connect(transport).catch((e) => {
2448
+ console.error("[zocket] MCP streamable connect error:", e);
2449
+ });
2450
+ } else {
2451
+ res.writeHead(400, { "Content-Type": "application/json" });
2452
+ res.end(JSON.stringify({
2453
+ jsonrpc: "2.0",
2454
+ error: { code: -32e3, message: "Bad Request: No valid session ID provided" },
2455
+ id: null
2456
+ }));
2457
+ return;
2458
+ }
2459
+ try {
2460
+ await transport.handleRequest(req, res, parsedBody);
2461
+ } catch (e) {
2462
+ console.error("[zocket] MCP streamable request error:", e);
2463
+ if (!res.headersSent) {
2464
+ res.writeHead(500, { "Content-Type": "application/json" });
2465
+ res.end(JSON.stringify({
2466
+ jsonrpc: "2.0",
2467
+ error: { code: -32603, message: "Internal server error" },
2468
+ id: null
2469
+ }));
2470
+ }
2471
+ }
2472
+ });
2473
+ streamableHttp.listen(opts.mcpStreamPort, opts.host, () => {
2474
+ console.log(`[zocket] mcp http://${opts.host}:${opts.mcpStreamPort}/mcp (streamable-http)`);
1979
2475
  console.log(`[zocket] ready \u2014 vault: ${vaultPath()}`);
1980
2476
  });
1981
2477
  }
1982
2478
  function buildCli() {
1983
2479
  const program = new Command("zocket").description("Local encrypted vault + MCP server for AI agent workflows").version("1.0.0");
1984
- program.command("start").description("Start web panel and MCP SSE server").option("--host <host>", "Bind host", "127.0.0.1").option("--web-port <port>", "Web panel port", "18001").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
2480
+ program.command("init").description("Initialize vault and config").action(async () => {
2481
+ createServices();
2482
+ console.log(`[zocket] initialized \u2014 vault: ${vaultPath()}`);
2483
+ });
2484
+ program.command("start").description("Start web panel + MCP SSE + MCP Streamable HTTP servers (use --no-web for MCP-only)").option("--host <host>", "Bind host", "127.0.0.1").option("--web-port <port>", "Web panel port", "18001").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").option("--no-web", "Disable web panel").action(async (opts) => {
1985
2485
  await cmdStart({
1986
2486
  host: opts.host,
1987
2487
  webPort: parseInt(opts.webPort, 10),
1988
2488
  mcpPort: parseInt(opts.mcpPort, 10),
1989
- mode: opts.mode
2489
+ mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
2490
+ mode: opts.mode,
2491
+ webEnabled: opts.web
2492
+ });
2493
+ });
2494
+ program.command("server").description("Start MCP servers without web panel").option("--host <host>", "Bind host", "127.0.0.1").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
2495
+ await cmdStart({
2496
+ host: opts.host,
2497
+ webPort: 18001,
2498
+ mcpPort: parseInt(opts.mcpPort, 10),
2499
+ mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
2500
+ mode: opts.mode,
2501
+ webEnabled: false
1990
2502
  });
1991
2503
  });
2504
+ const projects = program.command("projects").description("Manage projects");
2505
+ projects.command("list").action(async () => {
2506
+ const { vault } = createServices();
2507
+ const rows = await vault.listProjects();
2508
+ if (!rows.length) {
2509
+ console.log("No projects");
2510
+ return;
2511
+ }
2512
+ for (const r of rows) {
2513
+ console.log(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}`);
2514
+ }
2515
+ });
2516
+ projects.command("create <name>").option("--description <text>", "Description", "").option("--folder <path>", "Folder path", "").action(async (name, opts) => {
2517
+ const { vault } = createServices();
2518
+ await vault.createProject(name, opts.description ?? "");
2519
+ if (opts.folder) await vault.setFolder(name, opts.folder);
2520
+ console.log("Project created:", name);
2521
+ });
2522
+ projects.command("delete <name>").action(async (name) => {
2523
+ const { vault } = createServices();
2524
+ await vault.deleteProject(name);
2525
+ console.log("Project deleted:", name);
2526
+ });
2527
+ projects.command("set-folder <name> [path]").description('Set or clear folder path (use "-" to clear)').action(async (name, path) => {
2528
+ const { vault } = createServices();
2529
+ const value = path && path !== "-" ? path : void 0;
2530
+ await vault.setFolder(name, value);
2531
+ console.log("Folder updated:", name);
2532
+ });
2533
+ projects.command("set-domains <name> [domains]").description('Set or clear allowed domains (comma-separated, use "-" to clear)').action(async (name, domains) => {
2534
+ const { vault } = createServices();
2535
+ const value = domains && domains !== "-" ? domains.split(",").map((s) => s.trim()).filter(Boolean) : null;
2536
+ await vault.setAllowedDomains(name, value);
2537
+ console.log("Domains updated:", name);
2538
+ });
2539
+ const secrets = program.command("secrets").description("Manage secrets");
2540
+ secrets.command("list <project>").option("--show-values", "Include secret values", false).action(async (project, opts) => {
2541
+ const { vault } = createServices();
2542
+ const rows = await vault.listSecrets(project);
2543
+ if (!rows.length) {
2544
+ console.log("No secrets");
2545
+ return;
2546
+ }
2547
+ for (const r of rows) {
2548
+ if (opts.showValues) {
2549
+ const v = await vault.getSecretValue(project, r.key);
2550
+ console.log(`${r.key} ${v} ${r.description ?? ""}`);
2551
+ } else {
2552
+ console.log(`${r.key} ${r.description ?? ""}`);
2553
+ }
2554
+ }
2555
+ });
2556
+ secrets.command("get <project> <key>").action(async (project, key) => {
2557
+ const { vault } = createServices();
2558
+ const v = await vault.getSecretValue(project, key);
2559
+ console.log(v);
2560
+ });
2561
+ secrets.command("set <project> <key> <value>").option("--description <text>", "Description", "").action(async (project, key, value, opts) => {
2562
+ const { vault } = createServices();
2563
+ await vault.setSecret(project, key, value, opts.description ?? "");
2564
+ console.log("Secret saved:", key);
2565
+ });
2566
+ secrets.command("delete <project> <key>").action(async (project, key) => {
2567
+ const { vault } = createServices();
2568
+ await vault.deleteSecret(project, key);
2569
+ console.log("Secret deleted:", key);
2570
+ });
2571
+ program.command("tui").description("Interactive terminal UI for full management").action(async () => {
2572
+ const { vault, config, audit } = createServices();
2573
+ await runTui({ vault, config, audit });
2574
+ });
1992
2575
  return program;
1993
2576
  }
1994
2577