@ao_zorin/zocket 1.2.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/README.md CHANGED
@@ -41,6 +41,11 @@ zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-por
41
41
 
42
42
  Open `http://127.0.0.1:18001`.
43
43
 
44
+ MCP-only (no web panel):
45
+ ```bash
46
+ zocket server --host 127.0.0.1 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
47
+ ```
48
+
44
49
  ## CLI / TUI
45
50
 
46
51
  Full CLI management:
package/dist/zocket.js CHANGED
@@ -16,11 +16,26 @@ import { dirname } from "path";
16
16
  import { lock } from "proper-lockfile";
17
17
 
18
18
  // src/crypto.ts
19
- import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
19
+ import { randomBytes, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from "crypto";
20
20
  var VERSION = 1;
21
21
  var ALGORITHM = "aes-256-gcm";
22
22
  var IV_BYTES = 12;
23
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
+ }
24
39
  function encrypt(plaintext, key) {
25
40
  const iv = randomBytes(IV_BYTES);
26
41
  const cipher = createCipheriv(ALGORITHM, key, iv);
@@ -40,20 +55,53 @@ function decrypt(data, key) {
40
55
  decipher.setAuthTag(tag);
41
56
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
42
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
+ }
43
81
 
44
82
  // src/vault.ts
45
83
  var PROJECT_RE = /^[a-zA-Z0-9._-]+$/;
46
84
  var SECRET_RE = /^[A-Z_][A-Z0-9_]*$/;
47
85
  var VaultService = class {
48
- constructor(vaultPath2, lockFile, key) {
86
+ constructor(vaultPath2, lockFile, key, legacyKeyBase64, keyFilePath) {
49
87
  this.vaultPath = vaultPath2;
50
88
  this.lockFile = lockFile;
51
89
  this.key = key;
90
+ this.legacyKeyBase64 = legacyKeyBase64;
91
+ this.keyFilePath = keyFilePath;
52
92
  }
53
93
  load() {
54
94
  if (!existsSync(this.vaultPath)) return { version: 1, projects: {} };
55
95
  const raw = readFileSync(this.vaultPath);
56
- 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
+ }
57
105
  }
58
106
  save(data) {
59
107
  mkdirSync(dirname(this.vaultPath), { recursive: true });
@@ -64,6 +112,29 @@ var VaultService = class {
64
112
  this.save({ version: 1, projects: {} });
65
113
  }
66
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
+ }
67
138
  async withLock(fn) {
68
139
  mkdirSync(dirname(this.lockFile), { recursive: true });
69
140
  if (!existsSync(this.lockFile)) writeFileSync(this.lockFile, "");
@@ -1117,12 +1188,12 @@ function createMcpServer(services, options = {}) {
1117
1188
  // src/web.ts
1118
1189
  import { Hono } from "hono";
1119
1190
  import { getCookie, setCookie, deleteCookie } from "hono/cookie";
1120
- import { createHmac, randomBytes as randomBytes5 } from "crypto";
1191
+ import { createHmac as createHmac2, randomBytes as randomBytes5 } from "crypto";
1121
1192
  import { readdirSync, statSync, existsSync as existsSync4 } from "fs";
1122
1193
  import { join as join2, resolve as resolvePath, dirname as dirname4 } from "path";
1123
1194
 
1124
1195
  // src/auth.ts
1125
- import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual } from "crypto";
1196
+ import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual as timingSafeEqual2 } from "crypto";
1126
1197
  var ITERATIONS = 6e5;
1127
1198
  var KEY_LEN = 32;
1128
1199
  var DIGEST = "sha256";
@@ -1135,7 +1206,7 @@ function verifyPassword(password, hash, salt) {
1135
1206
  const derived = pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST);
1136
1207
  const expected = Buffer.from(hash, "hex");
1137
1208
  if (derived.length !== expected.length) return false;
1138
- return timingSafeEqual(derived, expected);
1209
+ return timingSafeEqual2(derived, expected);
1139
1210
  }
1140
1211
 
1141
1212
  // src/i18n.ts
@@ -1337,7 +1408,7 @@ function normalizeLang(lang) {
1337
1408
  var COOKIE = "zs";
1338
1409
  function signSession(data, secret) {
1339
1410
  const payload = Buffer.from(JSON.stringify(data)).toString("base64url");
1340
- 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);
1341
1412
  return `${payload}.${sig}`;
1342
1413
  }
1343
1414
  function parseSession(value, secret) {
@@ -1345,7 +1416,7 @@ function parseSession(value, secret) {
1345
1416
  if (dot === -1) return null;
1346
1417
  const payload = value.slice(0, dot);
1347
1418
  const sig = value.slice(dot + 1);
1348
- 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);
1349
1420
  if (sig !== expected) return null;
1350
1421
  try {
1351
1422
  return JSON.parse(Buffer.from(payload, "base64url").toString());
@@ -1925,177 +1996,360 @@ function lockPath(home = zocketHome()) {
1925
1996
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
1926
1997
  import { dirname as dirname5 } from "path";
1927
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
+ }
1928
2010
  function loadOrCreateKey(keyFile) {
1929
2011
  if (existsSync5(keyFile)) {
1930
2012
  const raw = readFileSync5(keyFile, "utf8").trim();
1931
- 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");
1932
2024
  }
1933
2025
  mkdirSync4(dirname5(keyFile), { recursive: true });
1934
2026
  const key = randomBytes6(32);
1935
2027
  writeFileSync5(keyFile, key.toString("hex"), { mode: 384 });
1936
- return key;
2028
+ return { key, source: "generated" };
1937
2029
  }
1938
2030
 
1939
2031
  // src/tui.ts
1940
- import readline from "readline/promises";
1941
- import { stdin as input, stdout as output } from "process";
1942
- async function promptMenu(rl, title, options) {
1943
- output.write(`
1944
- ${title}
1945
- `);
1946
- options.forEach((opt, i) => output.write(` ${i + 1}) ${opt}
1947
- `));
1948
- const answer = await rl.question("> ");
1949
- const idx = Number(answer.trim()) - 1;
1950
- return Number.isFinite(idx) && idx >= 0 && idx < options.length ? idx : -1;
1951
- }
1952
- async function promptInput(rl, label, fallback = "") {
1953
- const ans = await rl.question(`${label}${fallback ? ` (${fallback})` : ""}: `);
1954
- return ans.trim() || fallback;
1955
- }
1956
- async function promptConfirm(rl, label) {
1957
- const ans = await rl.question(`${label} [y/N]: `);
1958
- return ans.trim().toLowerCase() === "y";
2032
+ import blessed from "blessed";
2033
+ function safeText(value) {
2034
+ if (!value) return "";
2035
+ return String(value);
1959
2036
  }
1960
- async function pickProject(rl, vault) {
1961
- const rows = await vault.listProjects();
1962
- if (!rows.length) {
1963
- output.write("No projects\n");
1964
- return null;
1965
- }
1966
- const idx = await promptMenu(rl, "Select project", rows.map((r) => `${r.name} (${r.secret_count})`));
1967
- if (idx < 0) return null;
1968
- return rows[idx].name;
2037
+ function buildProjectLabel(row) {
2038
+ const count = row.secret_count ?? 0;
2039
+ return `${row.name} (${count})`;
1969
2040
  }
1970
- async function showProjects(vault) {
1971
- const rows = await vault.listProjects();
1972
- if (!rows.length) {
1973
- output.write("No projects\n");
1974
- return;
1975
- }
1976
- rows.forEach((r) => {
1977
- output.write(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}
1978
- `);
2041
+ async function runTui(services) {
2042
+ const { vault } = services;
2043
+ const screen = blessed.screen({
2044
+ smartCSR: true,
2045
+ title: "zocket"
1979
2046
  });
1980
- }
1981
- async function showSecrets(vault, project, showValues) {
1982
- const rows = await vault.listSecrets(project);
1983
- if (!rows.length) {
1984
- output.write("No secrets\n");
1985
- return;
1986
- }
1987
- for (const r of rows) {
1988
- if (showValues) {
1989
- const v = await vault.getSecretValue(project, r.key);
1990
- output.write(`${r.key} ${v} ${r.description ?? ""}
1991
- `);
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();
1992
2138
  } else {
1993
- output.write(`${r.key} ${r.description ?? ""}
1994
- `);
2139
+ projectList.style.border.fg = "cyan";
2140
+ secretTable.style.border.fg = "green";
2141
+ projectList.setLabel(" Projects ");
2142
+ secretTable.setLabel(" Secrets ");
2143
+ secretTable.focus();
1995
2144
  }
2145
+ screen.render();
1996
2146
  }
1997
- }
1998
- async function runTui(services) {
1999
- const rl = readline.createInterface({ input, output });
2000
- const { vault } = services;
2001
- try {
2002
- while (true) {
2003
- const main = await promptMenu(rl, "Zocket TUI", [
2004
- "Projects",
2005
- "Secrets",
2006
- "Exit"
2007
- ]);
2008
- if (main === 2) break;
2009
- if (main === 0) {
2010
- const idx = await promptMenu(rl, "Projects", [
2011
- "List",
2012
- "Create",
2013
- "Delete",
2014
- "Set folder path",
2015
- "Set allowed domains",
2016
- "Back"
2017
- ]);
2018
- if (idx === 5) continue;
2019
- if (idx === 0) await showProjects(vault);
2020
- if (idx === 1) {
2021
- const name = await promptInput(rl, "Project name");
2022
- const desc = await promptInput(rl, "Description", "");
2023
- const folder = await promptInput(rl, "Folder path", "");
2024
- await vault.createProject(name, desc);
2025
- if (folder) await vault.setFolder(name, folder);
2026
- output.write("Project created\n");
2027
- }
2028
- if (idx === 2) {
2029
- const name = await pickProject(rl, vault);
2030
- if (!name) continue;
2031
- if (await promptConfirm(rl, `Delete ${name}?`)) {
2032
- await vault.deleteProject(name);
2033
- output.write("Project deleted\n");
2034
- }
2035
- }
2036
- if (idx === 3) {
2037
- const name = await pickProject(rl, vault);
2038
- if (!name) continue;
2039
- const path = await promptInput(rl, "Folder path (empty to clear)", "");
2040
- await vault.setFolder(name, path || void 0);
2041
- output.write("Folder updated\n");
2042
- }
2043
- if (idx === 4) {
2044
- const name = await pickProject(rl, vault);
2045
- if (!name) continue;
2046
- const domains = await promptInput(rl, "Allowed domains (comma-separated, empty to clear)", "");
2047
- const value = domains ? domains.split(",").map((s) => s.trim()).filter(Boolean) : null;
2048
- await vault.setAllowedDomains(name, value);
2049
- output.write("Allowed domains updated\n");
2050
- }
2051
- }
2052
- if (main === 1) {
2053
- const project = await pickProject(rl, vault);
2054
- if (!project) continue;
2055
- const idx = await promptMenu(rl, `Secrets for ${project}`, [
2056
- "List (no values)",
2057
- "List (with values)",
2058
- "Add or update",
2059
- "Get value",
2060
- "Delete",
2061
- "Back"
2062
- ]);
2063
- if (idx === 5) continue;
2064
- if (idx === 0) await showSecrets(vault, project, false);
2065
- if (idx === 1) await showSecrets(vault, project, true);
2066
- if (idx === 2) {
2067
- const key = await promptInput(rl, "Key (UPPERCASE)", "");
2068
- const value = await promptInput(rl, "Value", "");
2069
- const desc = await promptInput(rl, "Description", "");
2070
- await vault.setSecret(project, key, value, desc);
2071
- output.write("Secret saved\n");
2072
- }
2073
- if (idx === 3) {
2074
- const key = await promptInput(rl, "Key", "");
2075
- const value = await vault.getSecretValue(project, key);
2076
- output.write(`${value}
2077
- `);
2078
- }
2079
- if (idx === 4) {
2080
- const key = await promptInput(rl, "Key", "");
2081
- if (await promptConfirm(rl, `Delete ${key}?`)) {
2082
- await vault.deleteSecret(project, key);
2083
- output.write("Secret deleted\n");
2084
- }
2085
- }
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
+ ];
2086
2210
  }
2087
2211
  }
2088
- } finally {
2089
- rl.close();
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;
2090
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();
2091
2345
  }
2092
2346
 
2093
2347
  // src/cli.ts
2094
2348
  function createServices() {
2095
2349
  const home = zocketHome();
2096
2350
  mkdirSync5(home, { recursive: true });
2097
- const key = loadOrCreateKey(keyPath());
2098
- const vault = new VaultService(vaultPath(), lockPath(), key);
2351
+ const keyMaterial = loadOrCreateKey(keyPath());
2352
+ const vault = new VaultService(vaultPath(), lockPath(), keyMaterial.key, keyMaterial.legacyBase64, keyPath());
2099
2353
  vault.ensureExists();
2100
2354
  const config = new ConfigStore(configPath());
2101
2355
  const audit = new AuditLogger(auditPath());
@@ -2107,10 +2361,14 @@ async function cmdStart(opts) {
2107
2361
  const cfg = config.ensureExists();
2108
2362
  const mode = opts.mode === "admin" ? "admin" : "metadata";
2109
2363
  const services = { vault, config, audit, mode };
2110
- const webApp = createWebApp({ vault, config, audit });
2111
- serve({ fetch: webApp.fetch, hostname: opts.host, port: opts.webPort }, () => {
2112
- console.log(`[zocket] web http://${opts.host}:${opts.webPort}`);
2113
- });
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
+ }
2114
2372
  const sessions = /* @__PURE__ */ new Map();
2115
2373
  const mcpHttp = createServer((req, res) => {
2116
2374
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -2223,13 +2481,24 @@ function buildCli() {
2223
2481
  createServices();
2224
2482
  console.log(`[zocket] initialized \u2014 vault: ${vaultPath()}`);
2225
2483
  });
2226
- program.command("start").description("Start web panel + MCP SSE + MCP Streamable HTTP servers").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").action(async (opts) => {
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) => {
2227
2485
  await cmdStart({
2228
2486
  host: opts.host,
2229
2487
  webPort: parseInt(opts.webPort, 10),
2230
2488
  mcpPort: parseInt(opts.mcpPort, 10),
2231
2489
  mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
2232
- mode: opts.mode
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
2233
2502
  });
2234
2503
  });
2235
2504
  const projects = program.command("projects").description("Manage projects");
package/docs/INSTALL.md CHANGED
@@ -12,6 +12,11 @@ This guide installs **zocket** (Node.js) as:
12
12
  curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash
13
13
  ```
14
14
 
15
+ MCP-only (no web panel):
16
+ ```bash
17
+ curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash -s -- --no-web
18
+ ```
19
+
15
20
  If you run from a local clone:
16
21
  ```bash
17
22
  bash scripts/install-zocket.sh --source local
@@ -22,6 +27,13 @@ bash scripts/install-zocket.sh --source local
22
27
  irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 | iex
23
28
  ```
24
29
 
30
+ MCP-only (no web panel):
31
+ ```powershell
32
+ $tmp = "$env:TEMP\\install-zocket.ps1"
33
+ irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 -OutFile $tmp
34
+ powershell -ExecutionPolicy Bypass -File $tmp -NoWeb
35
+ ```
36
+
25
37
  If you run from a local clone:
26
38
  ```powershell
27
39
  powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1 -Source Local
@@ -93,6 +105,11 @@ zocket init
93
105
  zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
94
106
  ```
95
107
 
108
+ MCP-only (no web panel):
109
+ ```bash
110
+ zocket server --host 127.0.0.1 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
111
+ ```
112
+
96
113
  ## 6) Systemd hardening on Linux (production)
97
114
 
98
115
  If you install with `--autostart system`, the installer creates and enables:
@@ -155,6 +172,7 @@ Or create manually:
155
172
  - task `Zocket` on logon
156
173
  - action:
157
174
  - `zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin`
175
+ - add `--no-web` for MCP-only
158
176
 
159
177
  ## 7) First web open
160
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ao_zorin/zocket",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Local encrypted vault + web panel + MCP server for AI agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "@anthropic-ai/sdk": "^0.78.0",
19
19
  "@hono/node-server": "^1.13.0",
20
20
  "@modelcontextprotocol/sdk": "^1.0.0",
21
+ "blessed": "^0.1.81",
21
22
  "commander": "^12.0.0",
22
23
  "hono": "^4.0.0",
23
24
  "proper-lockfile": "^4.1.2",
@@ -33,6 +34,7 @@
33
34
  },
34
35
  "devDependencies": {
35
36
  "@anthropic-ai/tokenizer": "^0.0.4",
37
+ "@types/blessed": "^0.1.24",
36
38
  "@types/node": "^20.0.0",
37
39
  "@types/proper-lockfile": "^4.1.4",
38
40
  "js-tiktoken": "^1.0.21",
@@ -11,7 +11,8 @@ Param(
11
11
  [int]$McpStreamPort = 18003,
12
12
  [ValidateSet("metadata", "admin")]
13
13
  [string]$McpMode = "admin",
14
- [bool]$EnableAutostart = $true
14
+ [bool]$EnableAutostart = $true,
15
+ [switch]$NoWeb
15
16
  )
16
17
 
17
18
  $ErrorActionPreference = "Stop"
@@ -63,12 +64,17 @@ $env:ZOCKET_HOME = $ZocketHome
63
64
  if ($EnableAutostart) {
64
65
  $taskName = "Zocket"
65
66
  $cmd = "\"$zocketBin\" start --host 127.0.0.1 --web-port $WebPort --mcp-port $McpPort --mcp-stream-port $McpStreamPort --mode $McpMode"
67
+ if ($NoWeb) { $cmd = "$cmd --no-web" }
66
68
  schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $taskName /TR $cmd | Out-Null
67
69
  }
68
70
 
69
71
  Write-Output "zocket installed successfully."
70
72
  Write-Output "zocket: $zocketBin"
71
73
  Write-Output "ZOCKET_HOME=$ZocketHome"
72
- Write-Output "web panel: http://127.0.0.1:$WebPort"
74
+ if ($NoWeb) {
75
+ Write-Output "web panel: disabled"
76
+ } else {
77
+ Write-Output "web panel: http://127.0.0.1:$WebPort"
78
+ }
73
79
  Write-Output "mcp sse: http://127.0.0.1:$McpPort/sse"
74
80
  Write-Output "mcp http: http://127.0.0.1:$McpStreamPort/mcp"
@@ -15,6 +15,7 @@ MCP_STREAM_PORT="${MCP_STREAM_PORT:-18003}"
15
15
  MCP_MODE="${MCP_MODE:-admin}" # metadata|admin
16
16
  AUTOSTART="${AUTOSTART:-user}" # user|system|none
17
17
  SERVICE_USER="${SERVICE_USER:-zocketd}"
18
+ WEB_ENABLED="${WEB_ENABLED:-true}"
18
19
 
19
20
  usage() {
20
21
  cat <<EOF
@@ -30,6 +31,7 @@ Options:
30
31
  --mcp-port <port>
31
32
  --mcp-stream-port <port>
32
33
  --mcp-mode <metadata|admin>
34
+ --no-web
33
35
  --autostart <user|system|none>
34
36
  --service-user <name>
35
37
  -h, --help
@@ -49,6 +51,7 @@ while [[ $# -gt 0 ]]; do
49
51
  --mcp-port) MCP_PORT="$2"; shift 2 ;;
50
52
  --mcp-stream-port) MCP_STREAM_PORT="$2"; shift 2 ;;
51
53
  --mcp-mode) MCP_MODE="$2"; shift 2 ;;
54
+ --no-web) WEB_ENABLED="false"; shift 1 ;;
52
55
  --autostart) AUTOSTART="$2"; shift 2 ;;
53
56
  --service-user) SERVICE_USER="$2"; shift 2 ;;
54
57
  -h|--help) usage; exit 0 ;;
@@ -242,6 +245,9 @@ EOF
242
245
  }
243
246
 
244
247
  EXEC_START="${ZOCKET_BIN} start --host 127.0.0.1 --web-port ${WEB_PORT} --mcp-port ${MCP_PORT} --mcp-stream-port ${MCP_STREAM_PORT} --mode ${MCP_MODE}"
248
+ if [[ "$WEB_ENABLED" == "false" ]]; then
249
+ EXEC_START="${EXEC_START} --no-web"
250
+ fi
245
251
 
246
252
  if [[ "$AUTOSTART" == "user" && "$OS" == linux* ]]; then
247
253
  USER_UNIT_DIR="$HOME/.config/systemd/user"
@@ -270,7 +276,7 @@ zocket: ${ZOCKET_BIN}
270
276
  ZOCKET_HOME=${ZOCKET_HOME_DIR}
271
277
 
272
278
  Default ports:
273
- web panel: http://127.0.0.1:${WEB_PORT}
279
+ web panel: http://127.0.0.1:${WEB_PORT}$( [[ "$WEB_ENABLED" == "false" ]] && printf ' (disabled)' )
274
280
  MCP SSE: http://127.0.0.1:${MCP_PORT}/sse
275
281
  MCP HTTP: http://127.0.0.1:${MCP_STREAM_PORT}/mcp
276
282