@kwonye/mcpx 0.1.105 → 0.1.106
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/adapters/codex.d.ts +2 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/json.d.ts +26 -0
- package/dist/adapters/pi.d.ts +9 -0
- package/dist/chunk-0xt563m1.js +126 -0
- package/dist/{chunk-btbww96n.js → chunk-1mrkz29t.js} +69 -56
- package/dist/{chunk-6mxr34tc.js → chunk-1v66g9d6.js} +4 -3
- package/dist/chunk-1yq3yb66.js +126 -0
- package/dist/chunk-27mq4agf.js +33 -0
- package/dist/{chunk-t4hqf6yh.js → chunk-4aafp444.js} +8136 -8262
- package/dist/chunk-4xym942z.js +36 -0
- package/dist/{chunk-tnhekcpv.js → chunk-56z18csx.js} +1 -1
- package/dist/{chunk-bnbz55g6.js → chunk-65kshh9w.js} +1 -1
- package/dist/{chunk-xydajvzg.js → chunk-7ky3rsw9.js} +1 -1
- package/dist/chunk-7rq88hb3.js +563 -0
- package/dist/chunk-8v13hqrp.js +825 -0
- package/dist/chunk-a2gen8bn.js +730 -0
- package/dist/{chunk-01s7rhx3.js → chunk-brxwa30j.js} +130 -113
- package/dist/chunk-chaj8a8b.js +45 -0
- package/dist/chunk-e96643mj.js +26 -0
- package/dist/{chunk-d2v9rxy0.js → chunk-ew06e41w.js} +12 -6
- package/dist/chunk-ezk8txcy.js +23 -0
- package/dist/{chunk-4y0t0tfj.js → chunk-grkz21sq.js} +29 -3
- package/dist/{chunk-12vgqm89.js → chunk-gvjxpk98.js} +1 -1
- package/dist/chunk-h9kp47rp.js +18 -0
- package/dist/chunk-hahs24dv.js +127 -0
- package/dist/{chunk-y9a1rx7j.js → chunk-jv0s0cp6.js} +5 -3
- package/dist/{chunk-enzmq965.js → chunk-n0462d52.js} +1 -1
- package/dist/chunk-nh6hhv29.js +138 -0
- package/dist/chunk-p7yzyt87.js +19426 -0
- package/dist/chunk-q86c5s6r.js +7 -0
- package/dist/chunk-qf8bj9y7.js +254 -0
- package/dist/chunk-t20f49g3.js +30 -0
- package/dist/{chunk-am903qjk.js → chunk-v041118a.js} +11 -5
- package/dist/{chunk-91vcwk5m.js → chunk-v14bg5yp.js} +1 -1
- package/dist/chunk-wvzap0na.js +18166 -0
- package/dist/chunk-wztmp5b4.js +30479 -0
- package/dist/chunk-xvqr1q9b.js +14 -0
- package/dist/{chunk-h158rkts.js → chunk-yj7mpczw.js} +17 -5
- package/dist/chunk-yr0txkw0.js +31 -0
- package/dist/cli.js +3879 -1061
- package/dist/core/adoption.d.ts +62 -0
- package/dist/core/analytics.d.ts +17 -0
- package/dist/core/consumers.d.ts +8 -0
- package/dist/core/control.d.ts +12 -0
- package/dist/core/daemon.d.ts +2 -2
- package/dist/core/file-state.d.ts +5 -0
- package/dist/core/harnesses.d.ts +25 -0
- package/dist/core/index.d.ts +12 -4
- package/dist/core/index.js +109 -71
- package/dist/core/managed-index-lock.d.ts +1 -1
- package/dist/core/management.d.ts +32 -0
- package/dist/core/native-document.d.ts +4 -0
- package/dist/core/operations.d.ts +52 -0
- package/dist/core/plugin-projections.d.ts +1 -0
- package/dist/core/runtime-entry.d.ts +6 -0
- package/dist/core/runtime-snapshot.d.ts +6 -0
- package/dist/core/secret-keychain.d.ts +31 -0
- package/dist/core/secrets.d.ts +12 -2
- package/dist/core/served-skills.d.ts +62 -0
- package/dist/core/status.d.ts +1 -0
- package/dist/core/sync-plan.d.ts +14 -0
- package/dist/core/sync.d.ts +3 -0
- package/dist/core/telemetry.d.ts +2 -2
- package/dist/core/tui.d.ts +20 -0
- package/dist/core/verify-upstreams.d.ts +4 -0
- package/dist/core/work-limiter.d.ts +10 -0
- package/dist/gateway/server.d.ts +2 -0
- package/dist/types.d.ts +62 -1
- package/dist/util/fs.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/adapters/opencode.d.ts +0 -11
- package/dist/chunk-56whm3hm.js +0 -4055
- package/dist/chunk-fw8dyt89.js +0 -344
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lockOwnerAlive
|
|
3
|
+
} from "./chunk-chaj8a8b.js";
|
|
4
|
+
import {
|
|
5
|
+
exports_external
|
|
6
|
+
} from "./chunk-p7yzyt87.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureParentDir,
|
|
9
|
+
getConfigPath,
|
|
10
|
+
getGatewayTokenPath,
|
|
11
|
+
getSecretsKeyPath,
|
|
12
|
+
getSecretsStorePath,
|
|
13
|
+
readJsonFile,
|
|
14
|
+
writeJsonAtomic
|
|
15
|
+
} from "./chunk-grkz21sq.js";
|
|
16
|
+
import {
|
|
17
|
+
isServerEnabled,
|
|
18
|
+
normalizeServerSpecEnabled
|
|
19
|
+
} from "./chunk-bjw71y54.js";
|
|
20
|
+
|
|
21
|
+
// src/core/registry.ts
|
|
22
|
+
import crypto from "node:crypto";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
function validateServerName(name) {
|
|
26
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(name)) {
|
|
27
|
+
throw new Error("Invalid server name. Use 1-63 chars: letters, numbers, '.', '-', '_' and start with alphanumeric.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function addServer(config, name, spec, force = false) {
|
|
31
|
+
validateServerName(name);
|
|
32
|
+
if (config.servers[name] && !force) {
|
|
33
|
+
throw new Error(`Server "${name}" already exists. Use --force to overwrite.`);
|
|
34
|
+
}
|
|
35
|
+
config.servers[name] = normalizeServerSpecEnabled(spec);
|
|
36
|
+
}
|
|
37
|
+
function removeServer(config, name, force = false) {
|
|
38
|
+
if (!config.servers[name] && !force) {
|
|
39
|
+
throw new Error(`Server "${name}" does not exist.`);
|
|
40
|
+
}
|
|
41
|
+
delete config.servers[name];
|
|
42
|
+
if (config.projects) {
|
|
43
|
+
for (const project of Object.values(config.projects)) {
|
|
44
|
+
if (project.disabledServers) {
|
|
45
|
+
project.disabledServers = project.disabledServers.filter((s) => s !== name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function updateServer(config, name, spec) {
|
|
51
|
+
validateServerName(name);
|
|
52
|
+
const existing = config.servers[name];
|
|
53
|
+
if (!existing) {
|
|
54
|
+
throw new Error(`Server "${name}" does not exist.`);
|
|
55
|
+
}
|
|
56
|
+
const updated = spec.scope === undefined && existing.scope !== undefined ? { ...spec, scope: existing.scope } : spec;
|
|
57
|
+
config.servers[name] = normalizeServerSpecEnabled(updated);
|
|
58
|
+
}
|
|
59
|
+
function setServerEnabled(config, name, enabled) {
|
|
60
|
+
const existing = config.servers[name];
|
|
61
|
+
if (!existing) {
|
|
62
|
+
throw new Error(`Server "${name}" does not exist.`);
|
|
63
|
+
}
|
|
64
|
+
config.servers[name] = {
|
|
65
|
+
...existing,
|
|
66
|
+
enabled
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getGatewayTokenSecretName(config) {
|
|
70
|
+
if (!/^secret:\/\/[A-Za-z0-9._-]+$/.test(config.gateway.tokenRef))
|
|
71
|
+
throw new Error("Invalid gateway token reference");
|
|
72
|
+
return config.gateway.tokenRef.slice("secret://".length) || "local_gateway_token";
|
|
73
|
+
}
|
|
74
|
+
function ensureGatewayToken(config, secrets) {
|
|
75
|
+
const secretName = getGatewayTokenSecretName(config);
|
|
76
|
+
const tokenPath = getGatewayTokenPath(secretName);
|
|
77
|
+
const envValue = process.env[`MCPX_SECRET_${secretName}`];
|
|
78
|
+
if (envValue && envValue.length > 0) {
|
|
79
|
+
return envValue;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const fileValue = fs.readFileSync(tokenPath, "utf8").trim();
|
|
83
|
+
if (fileValue) {
|
|
84
|
+
return fileValue;
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
const storeValue = secrets.getSecret(secretName);
|
|
88
|
+
if (storeValue) {
|
|
89
|
+
ensureParentDir(tokenPath);
|
|
90
|
+
fs.writeFileSync(tokenPath, storeValue, { mode: 384 });
|
|
91
|
+
return storeValue;
|
|
92
|
+
}
|
|
93
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
94
|
+
ensureParentDir(tokenPath);
|
|
95
|
+
fs.writeFileSync(tokenPath, token, { mode: 384 });
|
|
96
|
+
return token;
|
|
97
|
+
}
|
|
98
|
+
function rotateGatewayToken(config, secrets) {
|
|
99
|
+
const secretName = getGatewayTokenSecretName(config);
|
|
100
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
101
|
+
const tokenPath = getGatewayTokenPath(secretName);
|
|
102
|
+
ensureParentDir(tokenPath);
|
|
103
|
+
fs.writeFileSync(tokenPath, token, { mode: 384 });
|
|
104
|
+
secrets.removeSecret(secretName);
|
|
105
|
+
return token;
|
|
106
|
+
}
|
|
107
|
+
function registerProject(globalConfig, projectPath, name) {
|
|
108
|
+
if (!globalConfig.projects) {
|
|
109
|
+
globalConfig.projects = {};
|
|
110
|
+
}
|
|
111
|
+
const resolvedPath = path.resolve(projectPath);
|
|
112
|
+
const projectName = name?.trim() || path.basename(resolvedPath);
|
|
113
|
+
const existing = globalConfig.projects[resolvedPath];
|
|
114
|
+
globalConfig.projects[resolvedPath] = {
|
|
115
|
+
name: projectName,
|
|
116
|
+
path: resolvedPath,
|
|
117
|
+
disabledServers: existing?.disabledServers ?? []
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function unregisterProject(globalConfig, projectPath) {
|
|
121
|
+
if (globalConfig.projects) {
|
|
122
|
+
delete globalConfig.projects[path.resolve(projectPath)];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function setProjectServerEnabled(globalConfig, projectPath, serverName, enabled) {
|
|
126
|
+
if (!globalConfig.projects)
|
|
127
|
+
globalConfig.projects = {};
|
|
128
|
+
const resolvedPath = path.resolve(projectPath);
|
|
129
|
+
const project = globalConfig.projects[resolvedPath];
|
|
130
|
+
if (!project) {
|
|
131
|
+
throw new Error(`Project "${resolvedPath}" is not registered.`);
|
|
132
|
+
}
|
|
133
|
+
const serverSpec = globalConfig.servers[serverName];
|
|
134
|
+
if (!serverSpec) {
|
|
135
|
+
throw new Error(`Server "${serverName}" does not exist.`);
|
|
136
|
+
}
|
|
137
|
+
if (!project.disabledServers) {
|
|
138
|
+
project.disabledServers = [];
|
|
139
|
+
}
|
|
140
|
+
if (enabled) {
|
|
141
|
+
project.disabledServers = project.disabledServers.filter((s) => s !== serverName);
|
|
142
|
+
if (!isServerEnabled(serverSpec)) {
|
|
143
|
+
return { effective: false, reason: "globally_disabled" };
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
if (!project.disabledServers.includes(serverName)) {
|
|
147
|
+
project.disabledServers.push(serverName);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { effective: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/core/managed-index-lock.ts
|
|
154
|
+
import fs2 from "node:fs";
|
|
155
|
+
import path2 from "node:path";
|
|
156
|
+
function withManagedIndexLock(lockPath, fn, timeoutMs = 5000) {
|
|
157
|
+
fs2.mkdirSync(path2.dirname(lockPath), { recursive: true });
|
|
158
|
+
const deadline = Date.now() + timeoutMs;
|
|
159
|
+
while (true) {
|
|
160
|
+
try {
|
|
161
|
+
fs2.writeFileSync(lockPath, `${process.pid}
|
|
162
|
+
`, { flag: "wx" });
|
|
163
|
+
break;
|
|
164
|
+
} catch {
|
|
165
|
+
if (!lockOwnerAlive(lockPath)) {
|
|
166
|
+
try {
|
|
167
|
+
fs2.unlinkSync(lockPath);
|
|
168
|
+
} catch {}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
172
|
+
if (Date.now() > deadline) {
|
|
173
|
+
throw new Error(`Timed out waiting for managed-index lock: ${lockPath}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
return fn();
|
|
179
|
+
} finally {
|
|
180
|
+
try {
|
|
181
|
+
fs2.unlinkSync(lockPath);
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/core/errors.ts
|
|
187
|
+
class UpstreamError extends Error {
|
|
188
|
+
code;
|
|
189
|
+
upstream;
|
|
190
|
+
status;
|
|
191
|
+
wwwAuthenticate;
|
|
192
|
+
constructor(upstream, code, message, options) {
|
|
193
|
+
super(message);
|
|
194
|
+
this.name = "UpstreamError";
|
|
195
|
+
this.code = code;
|
|
196
|
+
this.upstream = upstream;
|
|
197
|
+
this.status = options?.status;
|
|
198
|
+
this.wwwAuthenticate = options?.wwwAuthenticate;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
class SecretNotFoundError extends Error {
|
|
203
|
+
code = "secret_missing";
|
|
204
|
+
secretName;
|
|
205
|
+
constructor(secretName) {
|
|
206
|
+
super(`Secret not found: ${secretName}. Run: mcpx secret set ${secretName}`);
|
|
207
|
+
this.name = "SecretNotFoundError";
|
|
208
|
+
this.secretName = secretName;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
var TIMEOUT_RE = /timed out after \d+ms/;
|
|
212
|
+
var UNREACHABLE_PATTERNS = [
|
|
213
|
+
/fetch failed/i,
|
|
214
|
+
/ECONNREFUSED/,
|
|
215
|
+
/ENOTFOUND/,
|
|
216
|
+
/EAI_AGAIN/,
|
|
217
|
+
/socket hang up/,
|
|
218
|
+
/ENOENT/,
|
|
219
|
+
/connect econnrefused/i,
|
|
220
|
+
/unable to connect/i
|
|
221
|
+
];
|
|
222
|
+
var UNREACHABLE_ERROR_CODES = new Set(["ConnectionRefused"]);
|
|
223
|
+
function classifyUpstreamError(upstream, error, wwwAuthenticate) {
|
|
224
|
+
if (error instanceof UpstreamError)
|
|
225
|
+
return error;
|
|
226
|
+
if (error instanceof SecretNotFoundError) {
|
|
227
|
+
return new UpstreamError(upstream, "secret_missing", error.message);
|
|
228
|
+
}
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
const maybeHttpError = error;
|
|
231
|
+
const httpStatus = maybeHttpError?.status ?? (typeof maybeHttpError?.code === "number" ? maybeHttpError.code : undefined);
|
|
232
|
+
if (httpStatus === 401 || httpStatus === 403) {
|
|
233
|
+
return new UpstreamError(upstream, "auth_required", message, {
|
|
234
|
+
status: httpStatus,
|
|
235
|
+
wwwAuthenticate
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (TIMEOUT_RE.test(message)) {
|
|
239
|
+
return new UpstreamError(upstream, "timeout", message);
|
|
240
|
+
}
|
|
241
|
+
if (typeof maybeHttpError?.code === "string" && UNREACHABLE_ERROR_CODES.has(maybeHttpError.code) || UNREACHABLE_PATTERNS.some((p) => p.test(message))) {
|
|
242
|
+
return new UpstreamError(upstream, "unreachable", message);
|
|
243
|
+
}
|
|
244
|
+
return new UpstreamError(upstream, "upstream_error", message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/core/secrets.ts
|
|
248
|
+
import crypto2 from "node:crypto";
|
|
249
|
+
import fs3 from "node:fs";
|
|
250
|
+
|
|
251
|
+
// src/core/secret-keychain.ts
|
|
252
|
+
import { spawnSync } from "node:child_process";
|
|
253
|
+
var KEYCHAIN_SERVICE = "com.kwonye.mcpx";
|
|
254
|
+
var KEYCHAIN_ACCOUNT = "secrets";
|
|
255
|
+
var KEYCHAIN_LABEL = "mcpx secrets";
|
|
256
|
+
var SECURITY_INTERACTIVE_MAX_LINE = 4094;
|
|
257
|
+
var MAX_KEYCHAIN_CHUNKS = 256;
|
|
258
|
+
var CHUNK_MANIFEST_PREFIX = "mcpx-keychain-chunks:";
|
|
259
|
+
var keychainPayloadSchema = exports_external.object({
|
|
260
|
+
v: exports_external.literal(1),
|
|
261
|
+
secrets: exports_external.record(exports_external.string(), exports_external.string())
|
|
262
|
+
});
|
|
263
|
+
var commandRunner = defaultSecretCommandRunner;
|
|
264
|
+
var platformOverride = null;
|
|
265
|
+
function currentPlatform() {
|
|
266
|
+
return platformOverride ?? process.platform;
|
|
267
|
+
}
|
|
268
|
+
function defaultSecretCommandRunner(request) {
|
|
269
|
+
const result = spawnSync(request.command, request.args, {
|
|
270
|
+
encoding: "utf8",
|
|
271
|
+
windowsHide: true,
|
|
272
|
+
...request.input !== undefined ? { input: request.input } : {}
|
|
273
|
+
});
|
|
274
|
+
if (result.error) {
|
|
275
|
+
const code = result.error.code;
|
|
276
|
+
if (code === "ENOENT") {
|
|
277
|
+
return { status: 127, stdout: "", stderr: `${request.command}: not found` };
|
|
278
|
+
}
|
|
279
|
+
throw result.error;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
status: result.status ?? 1,
|
|
283
|
+
stdout: result.stdout ?? "",
|
|
284
|
+
stderr: result.stderr ?? ""
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function keychainUnsupportedReason(platform = currentPlatform()) {
|
|
288
|
+
if (platform === "darwin" || platform === "linux")
|
|
289
|
+
return null;
|
|
290
|
+
return `The keychain backend is not supported on ${platform}. It is available on macOS (Keychain, via the security command) and Linux (libsecret, via secret-tool).`;
|
|
291
|
+
}
|
|
292
|
+
function assertKeychainSupported(platform) {
|
|
293
|
+
const reason = keychainUnsupportedReason(platform);
|
|
294
|
+
if (reason)
|
|
295
|
+
throw new Error(reason);
|
|
296
|
+
}
|
|
297
|
+
function missingToolMessage(command) {
|
|
298
|
+
if (command === "secret-tool") {
|
|
299
|
+
return "The keychain backend needs secret-tool from libsecret. Install libsecret-tools (Debian/Ubuntu) or libsecret (Fedora), or run `mcpx secret backend file`.";
|
|
300
|
+
}
|
|
301
|
+
if (command === "security") {
|
|
302
|
+
return "The keychain backend needs the macOS security command, but it was not found on PATH.";
|
|
303
|
+
}
|
|
304
|
+
return `The keychain backend could not run "${command}".`;
|
|
305
|
+
}
|
|
306
|
+
function throwIfFailed(result, command) {
|
|
307
|
+
if (result.status === 0)
|
|
308
|
+
return;
|
|
309
|
+
if (result.status === 127)
|
|
310
|
+
throw new Error(missingToolMessage(command));
|
|
311
|
+
const detail = result.stderr.trim() || `exited ${result.status}`;
|
|
312
|
+
throw new Error(`Keychain command failed (${command}): ${detail}`);
|
|
313
|
+
}
|
|
314
|
+
function isNotFound(result, platform) {
|
|
315
|
+
if (result.status === 0 || result.status === 127)
|
|
316
|
+
return false;
|
|
317
|
+
if (platform === "darwin" && result.status === 44)
|
|
318
|
+
return true;
|
|
319
|
+
const stderr = result.stderr.toLowerCase();
|
|
320
|
+
return stderr.includes("could not be found") || stderr.includes("item not found");
|
|
321
|
+
}
|
|
322
|
+
function chunkAccount(index) {
|
|
323
|
+
return `${KEYCHAIN_ACCOUNT}.${index}`;
|
|
324
|
+
}
|
|
325
|
+
function shellQuote(value) {
|
|
326
|
+
if (value.length === 0 || /['\\\r\n]/.test(value)) {
|
|
327
|
+
throw new Error("The keychain secret payload could not be encoded for the security command.");
|
|
328
|
+
}
|
|
329
|
+
return `'${value}'`;
|
|
330
|
+
}
|
|
331
|
+
function assertAccountName(account) {
|
|
332
|
+
if (!/^[A-Za-z0-9.]+$/.test(account)) {
|
|
333
|
+
throw new Error("Invalid keychain account name.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function darwinAddLine(account, password) {
|
|
337
|
+
assertAccountName(account);
|
|
338
|
+
return `add-generic-password -s ${KEYCHAIN_SERVICE} -a ${account} -l ${shellQuote(KEYCHAIN_LABEL)} -U -w ${shellQuote(password)}`;
|
|
339
|
+
}
|
|
340
|
+
function maxDarwinPasswordLength(account) {
|
|
341
|
+
const overhead = darwinAddLine(account, "x").length - 1;
|
|
342
|
+
return SECURITY_INTERACTIVE_MAX_LINE - overhead;
|
|
343
|
+
}
|
|
344
|
+
var CHUNK_PASSWORD_LIMIT = maxDarwinPasswordLength(chunkAccount(MAX_KEYCHAIN_CHUNKS));
|
|
345
|
+
function chunkCount(stored) {
|
|
346
|
+
if (!stored.startsWith(CHUNK_MANIFEST_PREFIX))
|
|
347
|
+
return null;
|
|
348
|
+
const raw = stored.slice(CHUNK_MANIFEST_PREFIX.length);
|
|
349
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
350
|
+
return null;
|
|
351
|
+
const count = Number(raw);
|
|
352
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_KEYCHAIN_CHUNKS)
|
|
353
|
+
return null;
|
|
354
|
+
return count;
|
|
355
|
+
}
|
|
356
|
+
function splitPayload(payload) {
|
|
357
|
+
const primaryLimit = maxDarwinPasswordLength(KEYCHAIN_ACCOUNT);
|
|
358
|
+
if (payload.length <= primaryLimit)
|
|
359
|
+
return [payload];
|
|
360
|
+
if (CHUNK_PASSWORD_LIMIT < 1) {
|
|
361
|
+
throw new Error("The macOS security command cannot accept a keychain password on standard input.");
|
|
362
|
+
}
|
|
363
|
+
const chunks = [];
|
|
364
|
+
for (let offset = 0;offset < payload.length; offset += CHUNK_PASSWORD_LIMIT) {
|
|
365
|
+
chunks.push(payload.slice(offset, offset + CHUNK_PASSWORD_LIMIT));
|
|
366
|
+
}
|
|
367
|
+
if (chunks.length > MAX_KEYCHAIN_CHUNKS) {
|
|
368
|
+
throw new Error(`The keychain secret payload is too large (${payload.length} characters) to store with the macOS security command.`);
|
|
369
|
+
}
|
|
370
|
+
return chunks;
|
|
371
|
+
}
|
|
372
|
+
function encodePayload(secrets) {
|
|
373
|
+
return Buffer.from(JSON.stringify({ v: 1, secrets }), "utf8").toString("base64");
|
|
374
|
+
}
|
|
375
|
+
function decodePayload(stored) {
|
|
376
|
+
const trimmed = stored.trim();
|
|
377
|
+
const unreadable = "The keychain secret payload could not be read. Refusing to change it.";
|
|
378
|
+
if (!trimmed)
|
|
379
|
+
throw new Error(unreadable);
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(Buffer.from(trimmed, "base64").toString("utf8"));
|
|
382
|
+
const result = keychainPayloadSchema.safeParse(parsed);
|
|
383
|
+
if (!result.success)
|
|
384
|
+
throw new Error(unreadable);
|
|
385
|
+
return result.data.secrets;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err instanceof Error && err.message === unreadable)
|
|
388
|
+
throw err;
|
|
389
|
+
throw new Error(unreadable);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function run(request) {
|
|
393
|
+
return commandRunner(request);
|
|
394
|
+
}
|
|
395
|
+
function readDarwinItem(account) {
|
|
396
|
+
const result = run({
|
|
397
|
+
command: "security",
|
|
398
|
+
args: ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"]
|
|
399
|
+
});
|
|
400
|
+
if (isNotFound(result, "darwin"))
|
|
401
|
+
return null;
|
|
402
|
+
throwIfFailed(result, "security");
|
|
403
|
+
return result.stdout.trim();
|
|
404
|
+
}
|
|
405
|
+
function readDarwinPayload() {
|
|
406
|
+
const primary = readDarwinItem(KEYCHAIN_ACCOUNT);
|
|
407
|
+
if (primary === null)
|
|
408
|
+
return null;
|
|
409
|
+
const count = chunkCount(primary);
|
|
410
|
+
if (count === null)
|
|
411
|
+
return primary;
|
|
412
|
+
let payload = "";
|
|
413
|
+
for (let index = 1;index <= count; index++) {
|
|
414
|
+
const part = readDarwinItem(chunkAccount(index));
|
|
415
|
+
if (!part)
|
|
416
|
+
throw new Error("The keychain secret payload could not be read. Refusing to change it.");
|
|
417
|
+
payload += part;
|
|
418
|
+
}
|
|
419
|
+
return payload;
|
|
420
|
+
}
|
|
421
|
+
function runSecurityLine(line) {
|
|
422
|
+
if (line.includes(`
|
|
423
|
+
`) || line.length > SECURITY_INTERACTIVE_MAX_LINE) {
|
|
424
|
+
throw new Error("Refusing to send a keychain command that the macOS security tool would truncate.");
|
|
425
|
+
}
|
|
426
|
+
const result = run({ command: "security", args: ["-i"], input: `${line}
|
|
427
|
+
` });
|
|
428
|
+
throwIfFailed(result, "security");
|
|
429
|
+
}
|
|
430
|
+
function deleteDarwinAccount(account) {
|
|
431
|
+
const result = run({
|
|
432
|
+
command: "security",
|
|
433
|
+
args: ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account]
|
|
434
|
+
});
|
|
435
|
+
if (isNotFound(result, "darwin"))
|
|
436
|
+
return "missing";
|
|
437
|
+
throwIfFailed(result, "security");
|
|
438
|
+
return "deleted";
|
|
439
|
+
}
|
|
440
|
+
function deleteExtraChunks(firstIndex) {
|
|
441
|
+
for (let index = firstIndex;index <= MAX_KEYCHAIN_CHUNKS; index++) {
|
|
442
|
+
if (deleteDarwinAccount(chunkAccount(index)) === "missing")
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function clearDarwinItems() {
|
|
447
|
+
deleteDarwinAccount(KEYCHAIN_ACCOUNT);
|
|
448
|
+
deleteExtraChunks(1);
|
|
449
|
+
}
|
|
450
|
+
function writeDarwinPayload(payload) {
|
|
451
|
+
const chunks = splitPayload(payload);
|
|
452
|
+
if (chunks.length === 1) {
|
|
453
|
+
const only = chunks[0];
|
|
454
|
+
if (only === undefined)
|
|
455
|
+
throw new Error("The keychain secret payload could not be encoded for the security command.");
|
|
456
|
+
runSecurityLine(darwinAddLine(KEYCHAIN_ACCOUNT, only));
|
|
457
|
+
deleteExtraChunks(1);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (let index = 0;index < chunks.length; index++) {
|
|
461
|
+
const chunk = chunks[index];
|
|
462
|
+
if (chunk === undefined)
|
|
463
|
+
throw new Error("The keychain secret payload could not be encoded for the security command.");
|
|
464
|
+
runSecurityLine(darwinAddLine(chunkAccount(index + 1), chunk));
|
|
465
|
+
}
|
|
466
|
+
runSecurityLine(darwinAddLine(KEYCHAIN_ACCOUNT, `${CHUNK_MANIFEST_PREFIX}${chunks.length}`));
|
|
467
|
+
deleteExtraChunks(chunks.length + 1);
|
|
468
|
+
}
|
|
469
|
+
function writeDarwinSecrets(payload) {
|
|
470
|
+
if (payload.length <= maxDarwinPasswordLength(KEYCHAIN_ACCOUNT)) {
|
|
471
|
+
writeDarwinPayload(payload);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const backup = readDarwinPayload();
|
|
475
|
+
try {
|
|
476
|
+
writeDarwinPayload(payload);
|
|
477
|
+
} catch (err) {
|
|
478
|
+
try {
|
|
479
|
+
if (backup === null)
|
|
480
|
+
clearDarwinItems();
|
|
481
|
+
else
|
|
482
|
+
writeDarwinPayload(backup);
|
|
483
|
+
} catch (restoreErr) {
|
|
484
|
+
const detail = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
|
|
485
|
+
throw new Error(`Keychain write failed and the previous keychain copy could not be restored: ${detail}`);
|
|
486
|
+
}
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function readKeychainSecrets() {
|
|
491
|
+
const platform = currentPlatform();
|
|
492
|
+
assertKeychainSupported(platform);
|
|
493
|
+
if (platform === "darwin") {
|
|
494
|
+
const payload = readDarwinPayload();
|
|
495
|
+
if (payload === null)
|
|
496
|
+
return {};
|
|
497
|
+
return decodePayload(payload);
|
|
498
|
+
}
|
|
499
|
+
const result = run({ command: "secret-tool", args: ["lookup", "service", KEYCHAIN_SERVICE, "account", KEYCHAIN_ACCOUNT] });
|
|
500
|
+
if (isNotFound(result, platform))
|
|
501
|
+
return {};
|
|
502
|
+
throwIfFailed(result, "secret-tool");
|
|
503
|
+
return decodePayload(result.stdout);
|
|
504
|
+
}
|
|
505
|
+
function writeKeychainSecrets(secrets) {
|
|
506
|
+
if (Object.keys(secrets).length === 0) {
|
|
507
|
+
clearKeychainSecrets();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const platform = currentPlatform();
|
|
511
|
+
assertKeychainSupported(platform);
|
|
512
|
+
const payload = encodePayload(secrets);
|
|
513
|
+
if (platform === "darwin") {
|
|
514
|
+
writeDarwinSecrets(payload);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const result = run({
|
|
518
|
+
command: "secret-tool",
|
|
519
|
+
args: ["store", "--label", KEYCHAIN_LABEL, "service", KEYCHAIN_SERVICE, "account", KEYCHAIN_ACCOUNT],
|
|
520
|
+
input: payload
|
|
521
|
+
});
|
|
522
|
+
throwIfFailed(result, "secret-tool");
|
|
523
|
+
}
|
|
524
|
+
function clearKeychainSecrets() {
|
|
525
|
+
const platform = currentPlatform();
|
|
526
|
+
assertKeychainSupported(platform);
|
|
527
|
+
if (platform === "darwin") {
|
|
528
|
+
clearDarwinItems();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const result = run({ command: "secret-tool", args: ["clear", "service", KEYCHAIN_SERVICE, "account", KEYCHAIN_ACCOUNT] });
|
|
532
|
+
if (result.status === 0 || isNotFound(result, platform))
|
|
533
|
+
return;
|
|
534
|
+
throwIfFailed(result, "secret-tool");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/core/secrets.ts
|
|
538
|
+
var ALGORITHM = "aes-256-gcm";
|
|
539
|
+
var UNREADABLE_FILE_STORE = "The encrypted credentials store could not be read. Restore its original key or backup before changing credentials. Existing files have been preserved.";
|
|
540
|
+
var SECRET_LOCK_TIMEOUT_MS = 120000;
|
|
541
|
+
var secretsDataSchema = exports_external.object({ secrets: exports_external.record(exports_external.string(), exports_external.string()) });
|
|
542
|
+
var secretBackendSchema = exports_external.object({
|
|
543
|
+
backend: exports_external.enum(["file", "keychain"]).default("file")
|
|
544
|
+
});
|
|
545
|
+
function describeSecretBackend(backend) {
|
|
546
|
+
if (backend === "keychain") {
|
|
547
|
+
return "OS keychain (macOS Keychain via the security command, or Linux libsecret via secret-tool). Secret values are written on the tool's standard input, not as process arguments. The operating system protects the secrets and may prompt for access. A locked session or a missing secret service cannot unlock them, and the background gateway waits while that prompt is open.";
|
|
548
|
+
}
|
|
549
|
+
return "AES-256-GCM encrypted file (secrets.json + secrets.key in the data directory). The encryption key sits next to the ciphertext, so other programs running as your user can read these secrets. No system prompts, and it works in headless sessions.";
|
|
550
|
+
}
|
|
551
|
+
function getSecretBackend(configPath = getConfigPath()) {
|
|
552
|
+
if (!fs3.existsSync(configPath))
|
|
553
|
+
return "file";
|
|
554
|
+
let raw;
|
|
555
|
+
try {
|
|
556
|
+
raw = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
557
|
+
} catch {
|
|
558
|
+
throw new Error(`Config file at ${configPath} is not valid JSON, so the secret backend could not be determined.`);
|
|
559
|
+
}
|
|
560
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
561
|
+
throw new Error(`Config file at ${configPath} is not a JSON object, so the secret backend could not be determined.`);
|
|
562
|
+
}
|
|
563
|
+
const secrets = raw.secrets;
|
|
564
|
+
if (secrets === undefined)
|
|
565
|
+
return "file";
|
|
566
|
+
const parsed = secretBackendSchema.safeParse(secrets);
|
|
567
|
+
if (!parsed.success) {
|
|
568
|
+
throw new Error(`Invalid secrets.backend in ${configPath}. Expected "file" or "keychain".`);
|
|
569
|
+
}
|
|
570
|
+
return parsed.data.backend;
|
|
571
|
+
}
|
|
572
|
+
function loadKey(keyPath) {
|
|
573
|
+
try {
|
|
574
|
+
const stored = fs3.readFileSync(keyPath, "utf8").trim();
|
|
575
|
+
return Buffer.from(stored, "base64");
|
|
576
|
+
} catch (err) {
|
|
577
|
+
if (err.code !== "ENOENT")
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
ensureParentDir(keyPath);
|
|
581
|
+
const key = crypto2.randomBytes(32);
|
|
582
|
+
try {
|
|
583
|
+
fs3.writeFileSync(keyPath, key.toString("base64") + `
|
|
584
|
+
`, { flag: "wx", mode: 384 });
|
|
585
|
+
} catch (err) {
|
|
586
|
+
if (err.code === "EEXIST") {
|
|
587
|
+
const stored = fs3.readFileSync(keyPath, "utf8").trim();
|
|
588
|
+
return Buffer.from(stored, "base64");
|
|
589
|
+
}
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
592
|
+
return key;
|
|
593
|
+
}
|
|
594
|
+
function readSecretsData(storePath, keyPath) {
|
|
595
|
+
const envelope = readJsonFile(storePath, null);
|
|
596
|
+
if (!envelope || envelope.version !== 1 || envelope.algorithm !== ALGORITHM)
|
|
597
|
+
return null;
|
|
598
|
+
try {
|
|
599
|
+
const key = Buffer.from(fs3.readFileSync(keyPath, "utf8").trim(), "base64");
|
|
600
|
+
const iv = Buffer.from(envelope.iv, "base64");
|
|
601
|
+
const authTag = Buffer.from(envelope.authTag, "base64");
|
|
602
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
603
|
+
const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
|
|
604
|
+
decipher.setAuthTag(authTag);
|
|
605
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
606
|
+
return secretsDataSchema.parse(JSON.parse(decrypted.toString("utf8")));
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function buildEnvelope(keyPath, data) {
|
|
612
|
+
const key = loadKey(keyPath);
|
|
613
|
+
const iv = crypto2.randomBytes(12);
|
|
614
|
+
const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
|
|
615
|
+
const plaintext = JSON.stringify(data);
|
|
616
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
617
|
+
return {
|
|
618
|
+
version: 1,
|
|
619
|
+
algorithm: ALGORITHM,
|
|
620
|
+
iv: iv.toString("base64"),
|
|
621
|
+
authTag: cipher.getAuthTag().toString("base64"),
|
|
622
|
+
ciphertext: encrypted.toString("base64")
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function readFileSecretsLoose(storePath, keyPath) {
|
|
626
|
+
return readSecretsData(storePath, keyPath)?.secrets ?? null;
|
|
627
|
+
}
|
|
628
|
+
function readFileSecretsStrict(storePath, keyPath) {
|
|
629
|
+
const map = readFileSecretsLoose(storePath, keyPath);
|
|
630
|
+
if (map === null && fs3.existsSync(storePath)) {
|
|
631
|
+
throw new Error(UNREADABLE_FILE_STORE);
|
|
632
|
+
}
|
|
633
|
+
return map ?? {};
|
|
634
|
+
}
|
|
635
|
+
function writeFileSecrets(storePath, keyPath, secrets) {
|
|
636
|
+
const envelope = buildEnvelope(keyPath, { secrets });
|
|
637
|
+
writeJsonAtomic(storePath, envelope);
|
|
638
|
+
}
|
|
639
|
+
function clearFileSecrets(storePath, keyPath) {
|
|
640
|
+
fs3.rmSync(storePath, { force: true });
|
|
641
|
+
fs3.rmSync(keyPath, { force: true });
|
|
642
|
+
}
|
|
643
|
+
function readBackendSecrets(backend, storePath, keyPath) {
|
|
644
|
+
if (backend === "keychain")
|
|
645
|
+
return readKeychainSecrets();
|
|
646
|
+
return readFileSecretsStrict(storePath, keyPath);
|
|
647
|
+
}
|
|
648
|
+
function writeBackendSecrets(backend, storePath, keyPath, secrets) {
|
|
649
|
+
if (backend === "keychain") {
|
|
650
|
+
writeKeychainSecrets(secrets);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
writeFileSecrets(storePath, keyPath, secrets);
|
|
654
|
+
}
|
|
655
|
+
function clearBackendSecrets(backend, storePath, keyPath) {
|
|
656
|
+
if (backend === "keychain") {
|
|
657
|
+
clearKeychainSecrets();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
clearFileSecrets(storePath, keyPath);
|
|
661
|
+
}
|
|
662
|
+
function secretsMatch(expected, actual) {
|
|
663
|
+
const expectedNames = Object.keys(expected);
|
|
664
|
+
if (expectedNames.length !== Object.keys(actual).length)
|
|
665
|
+
return false;
|
|
666
|
+
return expectedNames.every((name) => actual[name] === expected[name]);
|
|
667
|
+
}
|
|
668
|
+
function persistSecretBackend(backend, configPath) {
|
|
669
|
+
let raw;
|
|
670
|
+
if (fs3.existsSync(configPath)) {
|
|
671
|
+
let parsed;
|
|
672
|
+
try {
|
|
673
|
+
parsed = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
674
|
+
} catch {
|
|
675
|
+
throw new Error(`Config file at ${configPath} is not valid JSON, so the secret backend was not changed.`);
|
|
676
|
+
}
|
|
677
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
678
|
+
throw new Error(`Config file at ${configPath} is not a JSON object, so the secret backend was not changed.`);
|
|
679
|
+
}
|
|
680
|
+
raw = parsed;
|
|
681
|
+
} else {
|
|
682
|
+
raw = {};
|
|
683
|
+
}
|
|
684
|
+
const existing = typeof raw.secrets === "object" && raw.secrets !== null && !Array.isArray(raw.secrets) ? raw.secrets : {};
|
|
685
|
+
raw.secrets = { ...existing, backend };
|
|
686
|
+
writeJsonAtomic(configPath, raw);
|
|
687
|
+
}
|
|
688
|
+
function switchSecretBackend(target, configPath = getConfigPath()) {
|
|
689
|
+
if (target !== "file" && target !== "keychain") {
|
|
690
|
+
throw new Error(`Unknown secret backend "${String(target)}". Use "file" or "keychain".`);
|
|
691
|
+
}
|
|
692
|
+
const storePath = getSecretsStorePath();
|
|
693
|
+
const keyPath = getSecretsKeyPath();
|
|
694
|
+
return withManagedIndexLock(`${storePath}.lock`, () => {
|
|
695
|
+
const current = getSecretBackend(configPath);
|
|
696
|
+
if (current === target) {
|
|
697
|
+
return { backend: target, changed: false, migrated: [], preserved: [], removedFrom: null };
|
|
698
|
+
}
|
|
699
|
+
if (current === "keychain" || target === "keychain") {
|
|
700
|
+
const reason = keychainUnsupportedReason();
|
|
701
|
+
if (reason)
|
|
702
|
+
throw new Error(reason);
|
|
703
|
+
}
|
|
704
|
+
const source = readBackendSecrets(current, storePath, keyPath);
|
|
705
|
+
const destination = readBackendSecrets(target, storePath, keyPath);
|
|
706
|
+
const merged = { ...destination, ...source };
|
|
707
|
+
writeBackendSecrets(target, storePath, keyPath, merged);
|
|
708
|
+
const readBack = readBackendSecrets(target, storePath, keyPath);
|
|
709
|
+
if (!secretsMatch(merged, readBack)) {
|
|
710
|
+
try {
|
|
711
|
+
writeBackendSecrets(target, storePath, keyPath, destination);
|
|
712
|
+
} catch (restoreErr) {
|
|
713
|
+
const detail = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
|
|
714
|
+
throw new Error(`Migration to ${target} failed verification and the previous ${target} copy could not be restored: ${detail}. The ${current} backend was left unchanged.`);
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`Migration to ${target} failed verification. The ${current} backend was left unchanged.`);
|
|
717
|
+
}
|
|
718
|
+
persistSecretBackend(target, configPath);
|
|
719
|
+
try {
|
|
720
|
+
clearBackendSecrets(current, storePath, keyPath);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
723
|
+
throw new Error(`Secret backend is now ${target}, but secrets could not be removed from the ${current} store: ${detail}`);
|
|
724
|
+
}
|
|
725
|
+
const preserved = Object.keys(destination).filter((name) => !Object.prototype.hasOwnProperty.call(source, name)).sort();
|
|
726
|
+
return {
|
|
727
|
+
backend: target,
|
|
728
|
+
changed: true,
|
|
729
|
+
migrated: Object.keys(source).sort(),
|
|
730
|
+
preserved,
|
|
731
|
+
removedFrom: current
|
|
732
|
+
};
|
|
733
|
+
}, SECRET_LOCK_TIMEOUT_MS);
|
|
734
|
+
}
|
|
735
|
+
function parseSecretRef(ref) {
|
|
736
|
+
if (!ref.startsWith("secret://"))
|
|
737
|
+
return null;
|
|
738
|
+
const name = ref.slice("secret://".length);
|
|
739
|
+
return name.length > 0 ? name : null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
class SecretsManager {
|
|
743
|
+
storePath;
|
|
744
|
+
keyPath;
|
|
745
|
+
constructor(storePath = getSecretsStorePath(), keyPath = getSecretsKeyPath()) {
|
|
746
|
+
this.storePath = storePath;
|
|
747
|
+
this.keyPath = keyPath;
|
|
748
|
+
}
|
|
749
|
+
get backend() {
|
|
750
|
+
return getSecretBackend();
|
|
751
|
+
}
|
|
752
|
+
listSecretNames() {
|
|
753
|
+
if (this.backend === "keychain") {
|
|
754
|
+
return Object.keys(readKeychainSecrets()).sort();
|
|
755
|
+
}
|
|
756
|
+
return Object.keys(readFileSecretsLoose(this.storePath, this.keyPath) ?? {}).sort();
|
|
757
|
+
}
|
|
758
|
+
setSecret(name, value) {
|
|
759
|
+
return withManagedIndexLock(`${this.storePath}.lock`, () => {
|
|
760
|
+
if (this.backend === "keychain") {
|
|
761
|
+
const map2 = readKeychainSecrets();
|
|
762
|
+
map2[name] = value;
|
|
763
|
+
writeKeychainSecrets(map2);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const map = readFileSecretsStrict(this.storePath, this.keyPath);
|
|
767
|
+
map[name] = value;
|
|
768
|
+
writeFileSecrets(this.storePath, this.keyPath, map);
|
|
769
|
+
}, SECRET_LOCK_TIMEOUT_MS);
|
|
770
|
+
}
|
|
771
|
+
removeSecret(name) {
|
|
772
|
+
return withManagedIndexLock(`${this.storePath}.lock`, () => {
|
|
773
|
+
if (this.backend === "keychain") {
|
|
774
|
+
const map2 = readKeychainSecrets();
|
|
775
|
+
if (!(name in map2))
|
|
776
|
+
return;
|
|
777
|
+
delete map2[name];
|
|
778
|
+
writeKeychainSecrets(map2);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const map = readFileSecretsStrict(this.storePath, this.keyPath);
|
|
782
|
+
if (!(name in map))
|
|
783
|
+
return;
|
|
784
|
+
delete map[name];
|
|
785
|
+
writeFileSecrets(this.storePath, this.keyPath, map);
|
|
786
|
+
}, SECRET_LOCK_TIMEOUT_MS);
|
|
787
|
+
}
|
|
788
|
+
getSecret(name) {
|
|
789
|
+
const envOverride = process.env[`MCPX_SECRET_${name}`];
|
|
790
|
+
if (envOverride && envOverride.length > 0)
|
|
791
|
+
return envOverride;
|
|
792
|
+
if (this.backend === "keychain") {
|
|
793
|
+
return readKeychainSecrets()[name] ?? null;
|
|
794
|
+
}
|
|
795
|
+
return (readFileSecretsLoose(this.storePath, this.keyPath) ?? {})[name] ?? null;
|
|
796
|
+
}
|
|
797
|
+
resolveMaybeSecret(value) {
|
|
798
|
+
const secretName = parseSecretRef(value);
|
|
799
|
+
if (!secretName)
|
|
800
|
+
return value;
|
|
801
|
+
const secret = this.getSecret(secretName);
|
|
802
|
+
if (!secret)
|
|
803
|
+
throw new SecretNotFoundError(secretName);
|
|
804
|
+
return secret;
|
|
805
|
+
}
|
|
806
|
+
rotateLocalToken(secretName = "local_gateway_token") {
|
|
807
|
+
const token = crypto2.randomBytes(32).toString("base64url");
|
|
808
|
+
this.setSecret(secretName, token);
|
|
809
|
+
return token;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function readSecretValueFromStdin() {
|
|
813
|
+
if (process.stdin.isTTY) {
|
|
814
|
+
throw new Error("No value provided. Use --value or pipe a value via stdin.");
|
|
815
|
+
}
|
|
816
|
+
const content = fs3.readFileSync(process.stdin.fd, "utf8").trim();
|
|
817
|
+
if (content.length === 0) {
|
|
818
|
+
throw new Error("Empty secret value provided.");
|
|
819
|
+
}
|
|
820
|
+
return content;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export { withManagedIndexLock, UpstreamError, SecretNotFoundError, classifyUpstreamError, describeSecretBackend, getSecretBackend, switchSecretBackend, SecretsManager, readSecretValueFromStdin, validateServerName, addServer, removeServer, updateServer, setServerEnabled, getGatewayTokenSecretName, ensureGatewayToken, rotateGatewayToken, registerProject, unregisterProject, setProjectServerEnabled };
|
|
824
|
+
|
|
825
|
+
//# debugId=E4DB165C713155C264756E2164756E21
|