contextwise 0.1.0 → 0.2.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 +64 -0
- package/dist/{chunk-P7JW7EPW.js → chunk-5ZMX5TEM.js} +395 -1190
- package/dist/chunk-5ZMX5TEM.js.map +1 -0
- package/dist/chunk-OYCA37PJ.js +710 -0
- package/dist/chunk-OYCA37PJ.js.map +1 -0
- package/dist/chunk-WP7RH2Z2.js +417 -0
- package/dist/chunk-WP7RH2Z2.js.map +1 -0
- package/dist/cli.js +26 -89
- package/dist/cli.js.map +1 -1
- package/dist/client-3GQWPGI3.js +9 -0
- package/dist/client-3GQWPGI3.js.map +1 -0
- package/dist/index.d.ts +56 -24
- package/dist/index.js +49 -31
- package/dist/sync_manager-2WYXXGCN.js +10 -0
- package/dist/sync_manager-2WYXXGCN.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-P7JW7EPW.js.map +0 -1
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloudClient,
|
|
3
|
+
logger
|
|
4
|
+
} from "./chunk-WP7RH2Z2.js";
|
|
5
|
+
|
|
6
|
+
// src/cloud/sync_manager.ts
|
|
7
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
8
|
+
import { homedir as homedir5 } from "os";
|
|
9
|
+
import { join as join5 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/config/loader.ts
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { isAbsolute, join, resolve } from "path";
|
|
15
|
+
|
|
16
|
+
// src/config/schema.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var StdioUpstreamConfigSchema = z.object({
|
|
19
|
+
command: z.string(),
|
|
20
|
+
args: z.array(z.string()).default([]),
|
|
21
|
+
env: z.record(z.string()).default({}),
|
|
22
|
+
cwd: z.string().optional(),
|
|
23
|
+
autoRestart: z.boolean().default(true)
|
|
24
|
+
});
|
|
25
|
+
var HttpUpstreamConfigSchema = z.object({
|
|
26
|
+
url: z.string().url(),
|
|
27
|
+
headers: z.record(z.string()).default({}),
|
|
28
|
+
transport: z.enum(["streamable-http", "sse", "auto"]).default("auto"),
|
|
29
|
+
autoReconnect: z.boolean().default(true)
|
|
30
|
+
});
|
|
31
|
+
var UpstreamServerConfigSchema = z.union([
|
|
32
|
+
StdioUpstreamConfigSchema,
|
|
33
|
+
HttpUpstreamConfigSchema
|
|
34
|
+
]);
|
|
35
|
+
function warnIfInsecureHttp(urlStr, headers) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(urlStr);
|
|
38
|
+
if (parsed.protocol === "http:") {
|
|
39
|
+
const isLocalhost = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname);
|
|
40
|
+
if (!isLocalhost && headers) {
|
|
41
|
+
const hasAuthHeader = Object.keys(headers).some(
|
|
42
|
+
(h) => ["authorization", "cookie", "x-api-key", "api-key", "token"].includes(h.toLowerCase())
|
|
43
|
+
);
|
|
44
|
+
if (hasAuthHeader) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[SECURITY WARNING] Upstream URL "${urlStr}" is using unencrypted HTTP with sensitive authentication headers to non-localhost destination "${parsed.hostname}".`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isStdioUpstream(config) {
|
|
55
|
+
return "command" in config;
|
|
56
|
+
}
|
|
57
|
+
function isHttpUpstream(config) {
|
|
58
|
+
return "url" in config;
|
|
59
|
+
}
|
|
60
|
+
var ProxyConfigSchema = z.object({
|
|
61
|
+
transport: z.enum(["stdio", "sse", "http"]).default("stdio"),
|
|
62
|
+
port: z.number().int().min(1024).max(65535).default(3456),
|
|
63
|
+
logLevel: z.enum(["debug", "info", "warn", "error", "silent"]).default("info")
|
|
64
|
+
});
|
|
65
|
+
var RoutingConfigSchema = z.object({
|
|
66
|
+
strategy: z.enum(["hybrid", "bm25", "vector", "passthrough"]).default("hybrid"),
|
|
67
|
+
topK: z.number().int().min(1).max(50).default(5),
|
|
68
|
+
similarityThreshold: z.number().min(0).max(1).default(0.45),
|
|
69
|
+
pinnedTools: z.array(z.string()).default([]),
|
|
70
|
+
maxActiveTools: z.number().int().min(1).max(100).default(10),
|
|
71
|
+
enableBrowseServers: z.boolean().default(false),
|
|
72
|
+
enableAddServer: z.boolean().default(false),
|
|
73
|
+
enableCloudSync: z.boolean().default(true),
|
|
74
|
+
allowCustomCommands: z.boolean().default(false),
|
|
75
|
+
persistAddedServers: z.boolean().default(false)
|
|
76
|
+
});
|
|
77
|
+
var GuardrailsConfigSchema = z.object({
|
|
78
|
+
enableCache: z.boolean().default(true),
|
|
79
|
+
cacheTtlSeconds: z.number().int().min(1).default(120),
|
|
80
|
+
maxCallsPerMinute: z.number().int().min(1).default(60),
|
|
81
|
+
loopBreakerThreshold: z.number().int().min(1).default(3),
|
|
82
|
+
callTimeoutMs: z.number().int().min(1e3).default(3e4)
|
|
83
|
+
});
|
|
84
|
+
var ContextWiseConfigSchema = z.object({
|
|
85
|
+
$schema: z.string().optional(),
|
|
86
|
+
version: z.string().default("1.0.0"),
|
|
87
|
+
proxy: ProxyConfigSchema.default({}),
|
|
88
|
+
routing: RoutingConfigSchema.default({}),
|
|
89
|
+
guardrails: GuardrailsConfigSchema.default({}),
|
|
90
|
+
upstreams: z.record(UpstreamServerConfigSchema).default({})
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// src/config/loader.ts
|
|
94
|
+
var ConfigLoader = class {
|
|
95
|
+
/**
|
|
96
|
+
* Discovers and loads configuration from contextwise.json or imported client configs.
|
|
97
|
+
*/
|
|
98
|
+
static load(options = {}) {
|
|
99
|
+
const cwd = options.cwd ?? process.cwd();
|
|
100
|
+
if (process.env.CONTEXTWISE_CONFIG && !options.configPath) {
|
|
101
|
+
const envPath = isAbsolute(process.env.CONTEXTWISE_CONFIG) ? process.env.CONTEXTWISE_CONFIG : resolve(cwd, process.env.CONTEXTWISE_CONFIG);
|
|
102
|
+
if (existsSync(envPath)) {
|
|
103
|
+
return this.parseConfigFile(envPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (options.configPath) {
|
|
107
|
+
const explicitPath = isAbsolute(options.configPath) ? options.configPath : resolve(cwd, options.configPath);
|
|
108
|
+
if (existsSync(explicitPath)) {
|
|
109
|
+
return this.parseConfigFile(explicitPath);
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Configuration file not found at: ${explicitPath}`);
|
|
112
|
+
}
|
|
113
|
+
const candidatePaths = [
|
|
114
|
+
resolve(cwd, "contextwise.json"),
|
|
115
|
+
resolve(cwd, ".contextwise.json"),
|
|
116
|
+
resolve(cwd, ".contextwise/config.json"),
|
|
117
|
+
resolve(homedir(), ".contextwise/config.json")
|
|
118
|
+
];
|
|
119
|
+
for (const candidate of candidatePaths) {
|
|
120
|
+
if (existsSync(candidate)) {
|
|
121
|
+
logger.debug(`Loaded configuration from ${candidate}`);
|
|
122
|
+
return this.parseConfigFile(candidate);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (options.autoImport !== false) {
|
|
126
|
+
const imported = this.autoImportClientConfigs(cwd);
|
|
127
|
+
if (imported && Object.keys(imported.upstreams).length > 0) {
|
|
128
|
+
logger.info(`Auto-discovered MCP servers from existing client configurations`);
|
|
129
|
+
return imported;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
logger.debug("No configuration file found. Using defaults.");
|
|
133
|
+
return ContextWiseConfigSchema.parse({});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parses and validates a JSON file against ContextWiseConfigSchema.
|
|
137
|
+
*/
|
|
138
|
+
static parseConfigFile(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
141
|
+
const parsed = JSON.parse(raw);
|
|
142
|
+
return ContextWiseConfigSchema.parse(parsed);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
145
|
+
throw new Error(`Failed to parse config at ${filePath}: ${msg}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Scans for Cursor and Claude Desktop configs to auto-import upstreams.
|
|
150
|
+
*/
|
|
151
|
+
static autoImportClientConfigs(cwd) {
|
|
152
|
+
const upstreams = {};
|
|
153
|
+
const cursorMcp = resolve(cwd, ".cursor", "mcp.json");
|
|
154
|
+
if (existsSync(cursorMcp)) {
|
|
155
|
+
try {
|
|
156
|
+
const raw = JSON.parse(readFileSync(cursorMcp, "utf-8"));
|
|
157
|
+
if (raw.mcpServers && typeof raw.mcpServers === "object") {
|
|
158
|
+
for (const [name, server] of Object.entries(raw.mcpServers)) {
|
|
159
|
+
if (name === "contextwise") continue;
|
|
160
|
+
const s = server;
|
|
161
|
+
if (typeof s.command === "string") {
|
|
162
|
+
upstreams[name] = {
|
|
163
|
+
command: s.command,
|
|
164
|
+
args: Array.isArray(s.args) ? s.args : [],
|
|
165
|
+
env: s.env ?? {},
|
|
166
|
+
autoRestart: true
|
|
167
|
+
};
|
|
168
|
+
} else if (typeof s.url === "string") {
|
|
169
|
+
upstreams[name] = {
|
|
170
|
+
url: s.url,
|
|
171
|
+
headers: s.headers ?? {},
|
|
172
|
+
transport: "auto",
|
|
173
|
+
autoReconnect: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.warn(`Failed reading ${cursorMcp}: ${err}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const claudePath = this.getClaudeDesktopConfigPath();
|
|
183
|
+
if (claudePath && existsSync(claudePath)) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = JSON.parse(readFileSync(claudePath, "utf-8"));
|
|
186
|
+
if (raw.mcpServers && typeof raw.mcpServers === "object") {
|
|
187
|
+
for (const [name, server] of Object.entries(raw.mcpServers)) {
|
|
188
|
+
if (name === "contextwise") continue;
|
|
189
|
+
const s = server;
|
|
190
|
+
if (typeof s.command === "string" && !upstreams[name]) {
|
|
191
|
+
upstreams[name] = {
|
|
192
|
+
command: s.command,
|
|
193
|
+
args: Array.isArray(s.args) ? s.args : [],
|
|
194
|
+
env: s.env ?? {},
|
|
195
|
+
autoRestart: true
|
|
196
|
+
};
|
|
197
|
+
} else if (typeof s.url === "string" && !upstreams[name]) {
|
|
198
|
+
upstreams[name] = {
|
|
199
|
+
url: s.url,
|
|
200
|
+
headers: s.headers ?? {},
|
|
201
|
+
transport: "auto",
|
|
202
|
+
autoReconnect: true
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logger.warn(`Failed reading Claude config at ${claudePath}: ${err}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (Object.keys(upstreams).length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return ContextWiseConfigSchema.parse({
|
|
215
|
+
upstreams
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
static getClaudeDesktopConfigPath() {
|
|
219
|
+
const platform2 = process.platform;
|
|
220
|
+
const home = homedir();
|
|
221
|
+
if (platform2 === "win32") {
|
|
222
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
223
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
224
|
+
} else if (platform2 === "darwin") {
|
|
225
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
226
|
+
} else {
|
|
227
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/vault/file_driver.ts
|
|
233
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
234
|
+
import { homedir as homedir3 } from "os";
|
|
235
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
236
|
+
|
|
237
|
+
// src/vault/crypto.ts
|
|
238
|
+
import {
|
|
239
|
+
createCipheriv,
|
|
240
|
+
createDecipheriv,
|
|
241
|
+
createHash,
|
|
242
|
+
createPrivateKey,
|
|
243
|
+
createPublicKey,
|
|
244
|
+
diffieHellman,
|
|
245
|
+
generateKeyPairSync,
|
|
246
|
+
pbkdf2,
|
|
247
|
+
randomBytes
|
|
248
|
+
} from "crypto";
|
|
249
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
250
|
+
import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
|
|
251
|
+
import { join as join2 } from "path";
|
|
252
|
+
function encryptAesGcm(plaintext, key) {
|
|
253
|
+
if (key.length !== 32) {
|
|
254
|
+
throw new Error(`Invalid AES-256 key length: expected 32 bytes, got ${key.length}`);
|
|
255
|
+
}
|
|
256
|
+
const iv = randomBytes(12);
|
|
257
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
258
|
+
let ciphertext = cipher.update(plaintext, "utf-8", "base64");
|
|
259
|
+
ciphertext += cipher.final("base64");
|
|
260
|
+
const authTag = cipher.getAuthTag();
|
|
261
|
+
return {
|
|
262
|
+
iv: iv.toString("base64"),
|
|
263
|
+
authTag: authTag.toString("base64"),
|
|
264
|
+
ciphertext
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function decryptAesGcm(ciphertext, key, ivBase64, authTagBase64) {
|
|
268
|
+
if (key.length !== 32) {
|
|
269
|
+
throw new Error(`Invalid AES-256 key length: expected 32 bytes, got ${key.length}`);
|
|
270
|
+
}
|
|
271
|
+
const iv = Buffer.from(ivBase64, "base64");
|
|
272
|
+
const authTag = Buffer.from(authTagBase64, "base64");
|
|
273
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
274
|
+
decipher.setAuthTag(authTag);
|
|
275
|
+
let decrypted = decipher.update(ciphertext, "base64", "utf-8");
|
|
276
|
+
decrypted += decipher.final("utf-8");
|
|
277
|
+
return decrypted;
|
|
278
|
+
}
|
|
279
|
+
var VAULT_PBKDF2_ITERATIONS = 21e4;
|
|
280
|
+
function deriveKeyFromPassphrase(passphrase, salt, iterations = VAULT_PBKDF2_ITERATIONS) {
|
|
281
|
+
return new Promise((resolve2, reject) => {
|
|
282
|
+
pbkdf2(passphrase, salt, iterations, 32, "sha512", (err, derivedKey) => {
|
|
283
|
+
if (err) reject(err);
|
|
284
|
+
else resolve2(derivedKey);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async function getOrCreateMachineKey() {
|
|
289
|
+
const saltDir = join2(homedir2(), ".contextwise");
|
|
290
|
+
const saltPath = join2(saltDir, ".machine_salt");
|
|
291
|
+
let salt;
|
|
292
|
+
if (existsSync2(saltPath)) {
|
|
293
|
+
salt = readFileSync2(saltPath);
|
|
294
|
+
} else {
|
|
295
|
+
mkdirSync(saltDir, { recursive: true });
|
|
296
|
+
salt = randomBytes(32);
|
|
297
|
+
writeFileSync(saltPath, salt, { mode: 384 });
|
|
298
|
+
}
|
|
299
|
+
if (process.env.CONTEXTWISE_VAULT_PASSPHRASE) {
|
|
300
|
+
return deriveKeyFromPassphrase(process.env.CONTEXTWISE_VAULT_PASSPHRASE, salt, VAULT_PBKDF2_ITERATIONS);
|
|
301
|
+
}
|
|
302
|
+
const user = (() => {
|
|
303
|
+
try {
|
|
304
|
+
return userInfo().username;
|
|
305
|
+
} catch {
|
|
306
|
+
return "default_user";
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
const machineId = `${hostname()}-${user}-${platform()}-${arch()}-contextwise-vault-v1`;
|
|
310
|
+
return deriveKeyFromPassphrase(machineId, salt, VAULT_PBKDF2_ITERATIONS);
|
|
311
|
+
}
|
|
312
|
+
function generateKeyPairX25519() {
|
|
313
|
+
const { publicKey, privateKey } = generateKeyPairSync("x25519", {
|
|
314
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
315
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
316
|
+
});
|
|
317
|
+
return { publicKey, privateKey };
|
|
318
|
+
}
|
|
319
|
+
function deriveSharedSecretX25519(privateKeyPem, publicKeyPem) {
|
|
320
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
321
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
322
|
+
const shared = diffieHellman({
|
|
323
|
+
privateKey,
|
|
324
|
+
publicKey
|
|
325
|
+
});
|
|
326
|
+
return createHash("sha256").update(shared).digest();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/vault/file_driver.ts
|
|
330
|
+
function getDefaultVaultFilePath() {
|
|
331
|
+
if (process.env.CONTEXTWISE_VAULT_PATH) {
|
|
332
|
+
return process.env.CONTEXTWISE_VAULT_PATH;
|
|
333
|
+
}
|
|
334
|
+
if (process.env.CONTEXTWISE_STORAGE_DIR) {
|
|
335
|
+
return join3(process.env.CONTEXTWISE_STORAGE_DIR, "vault.enc.json");
|
|
336
|
+
}
|
|
337
|
+
return join3(homedir3(), ".contextwise", "vault.enc.json");
|
|
338
|
+
}
|
|
339
|
+
var EncryptedFileVaultDriver = class {
|
|
340
|
+
name = "encrypted_file";
|
|
341
|
+
filePath;
|
|
342
|
+
keyPromise;
|
|
343
|
+
cache = null;
|
|
344
|
+
constructor(filePath, customKey) {
|
|
345
|
+
this.filePath = filePath ?? getDefaultVaultFilePath();
|
|
346
|
+
this.keyPromise = customKey ? Promise.resolve(customKey) : getOrCreateMachineKey();
|
|
347
|
+
}
|
|
348
|
+
async isAvailable() {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
async load() {
|
|
352
|
+
if (this.cache) {
|
|
353
|
+
return this.cache;
|
|
354
|
+
}
|
|
355
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
356
|
+
if (!existsSync3(this.filePath)) {
|
|
357
|
+
return this.cache;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const raw = readFileSync3(this.filePath, "utf-8");
|
|
361
|
+
const payload = JSON.parse(raw);
|
|
362
|
+
if (payload.cipher !== "aes-256-gcm" || !payload.data) {
|
|
363
|
+
logger.warn(`Corrupt or incompatible vault file at ${this.filePath}`);
|
|
364
|
+
return this.cache;
|
|
365
|
+
}
|
|
366
|
+
const key = await this.keyPromise;
|
|
367
|
+
const decryptedJson = decryptAesGcm(payload.data, key, payload.iv, payload.authTag);
|
|
368
|
+
const entries = JSON.parse(decryptedJson);
|
|
369
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
370
|
+
this.cache.set(k, v);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
logger.warn(`Failed to decrypt vault file at ${this.filePath}: ${err}`);
|
|
374
|
+
}
|
|
375
|
+
return this.cache;
|
|
376
|
+
}
|
|
377
|
+
async persist() {
|
|
378
|
+
if (!this.cache) return;
|
|
379
|
+
const dir = dirname2(this.filePath);
|
|
380
|
+
if (!existsSync3(dir)) {
|
|
381
|
+
mkdirSync2(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
const key = await this.keyPromise;
|
|
384
|
+
const entries = {};
|
|
385
|
+
for (const [k, v] of this.cache.entries()) {
|
|
386
|
+
entries[k] = v;
|
|
387
|
+
}
|
|
388
|
+
const plaintext = JSON.stringify(entries);
|
|
389
|
+
const { iv, authTag, ciphertext } = encryptAesGcm(plaintext, key);
|
|
390
|
+
const payload = {
|
|
391
|
+
version: 1,
|
|
392
|
+
kdf: {
|
|
393
|
+
algorithm: "pbkdf2-sha512",
|
|
394
|
+
salt: "machine-bound",
|
|
395
|
+
iterations: VAULT_PBKDF2_ITERATIONS
|
|
396
|
+
},
|
|
397
|
+
cipher: "aes-256-gcm",
|
|
398
|
+
iv,
|
|
399
|
+
authTag,
|
|
400
|
+
data: ciphertext
|
|
401
|
+
};
|
|
402
|
+
const tempPath = `${this.filePath}.${Date.now()}.tmp`;
|
|
403
|
+
writeFileSync2(tempPath, JSON.stringify(payload, null, 2), { mode: 384, encoding: "utf-8" });
|
|
404
|
+
try {
|
|
405
|
+
renameSync(tempPath, this.filePath);
|
|
406
|
+
} catch {
|
|
407
|
+
writeFileSync2(this.filePath, JSON.stringify(payload, null, 2), { mode: 384, encoding: "utf-8" });
|
|
408
|
+
try {
|
|
409
|
+
unlinkSync(tempPath);
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async get(key) {
|
|
415
|
+
const store = await this.load();
|
|
416
|
+
const entry = store.get(key);
|
|
417
|
+
return entry ? entry.value : null;
|
|
418
|
+
}
|
|
419
|
+
async set(key, value, scope = "personal") {
|
|
420
|
+
const store = await this.load();
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
const existing = store.get(key);
|
|
423
|
+
store.set(key, {
|
|
424
|
+
value,
|
|
425
|
+
metadata: {
|
|
426
|
+
key,
|
|
427
|
+
scope,
|
|
428
|
+
backend: this.name,
|
|
429
|
+
createdAt: existing?.metadata.createdAt ?? now,
|
|
430
|
+
updatedAt: now
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
await this.persist();
|
|
434
|
+
logger.debug(`Stored secret "${key}" in ${this.name} vault`);
|
|
435
|
+
}
|
|
436
|
+
async delete(key) {
|
|
437
|
+
const store = await this.load();
|
|
438
|
+
if (!store.has(key)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
store.delete(key);
|
|
442
|
+
await this.persist();
|
|
443
|
+
logger.debug(`Deleted secret "${key}" from ${this.name} vault`);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
async list() {
|
|
447
|
+
const store = await this.load();
|
|
448
|
+
return Array.from(store.values()).map((e) => e.metadata);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Resets and clears all cached and persisted secrets.
|
|
452
|
+
*/
|
|
453
|
+
async clear() {
|
|
454
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
455
|
+
if (existsSync3(this.filePath)) {
|
|
456
|
+
try {
|
|
457
|
+
unlinkSync(this.filePath);
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/cloud/crypto.ts
|
|
465
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
466
|
+
import { homedir as homedir4 } from "os";
|
|
467
|
+
import { join as join4 } from "path";
|
|
468
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
469
|
+
function createEnvelope(workspaceKey, recipientPublicKeyPem) {
|
|
470
|
+
const ephemeral = generateKeyPairX25519();
|
|
471
|
+
const sharedKey = deriveSharedSecretX25519(ephemeral.privateKey, recipientPublicKeyPem);
|
|
472
|
+
const { iv, authTag, ciphertext } = encryptAesGcm(
|
|
473
|
+
workspaceKey.toString("base64"),
|
|
474
|
+
sharedKey
|
|
475
|
+
);
|
|
476
|
+
return {
|
|
477
|
+
recipientPublicKey: recipientPublicKeyPem,
|
|
478
|
+
ephemeralPublicKey: ephemeral.publicKey,
|
|
479
|
+
iv,
|
|
480
|
+
authTag,
|
|
481
|
+
ciphertext
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function openEnvelope(envelope, recipientPrivateKeyPem) {
|
|
485
|
+
const sharedKey = deriveSharedSecretX25519(
|
|
486
|
+
recipientPrivateKeyPem,
|
|
487
|
+
envelope.ephemeralPublicKey
|
|
488
|
+
);
|
|
489
|
+
const decryptedBase64 = decryptAesGcm(
|
|
490
|
+
envelope.ciphertext,
|
|
491
|
+
sharedKey,
|
|
492
|
+
envelope.iv,
|
|
493
|
+
envelope.authTag
|
|
494
|
+
);
|
|
495
|
+
return Buffer.from(decryptedBase64, "base64");
|
|
496
|
+
}
|
|
497
|
+
function getOrCreateDeviceIdentity(storageDir) {
|
|
498
|
+
const dir = storageDir || process.env.CONTEXTWISE_STORAGE_DIR || join4(homedir4(), ".contextwise");
|
|
499
|
+
const filePath = join4(dir, "device.json");
|
|
500
|
+
if (existsSync4(filePath)) {
|
|
501
|
+
try {
|
|
502
|
+
const data = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
503
|
+
if (data.deviceId && data.publicKey && data.privateKey) {
|
|
504
|
+
return data;
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!existsSync4(dir)) {
|
|
510
|
+
mkdirSync3(dir, { recursive: true });
|
|
511
|
+
}
|
|
512
|
+
const { publicKey, privateKey } = generateKeyPairX25519();
|
|
513
|
+
const deviceId = `cw_dev_${randomBytes2(8).toString("hex")}`;
|
|
514
|
+
const identity = {
|
|
515
|
+
deviceId,
|
|
516
|
+
publicKey,
|
|
517
|
+
privateKey
|
|
518
|
+
};
|
|
519
|
+
writeFileSync3(filePath, JSON.stringify(identity, null, 2), {
|
|
520
|
+
mode: 384,
|
|
521
|
+
encoding: "utf-8"
|
|
522
|
+
});
|
|
523
|
+
return identity;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/cloud/sync_manager.ts
|
|
527
|
+
var SyncManager = class {
|
|
528
|
+
syncStatePath;
|
|
529
|
+
localConfigPath;
|
|
530
|
+
constructor(storageDir, localConfigPath) {
|
|
531
|
+
const dir = storageDir || process.env.CONTEXTWISE_STORAGE_DIR || join5(homedir5(), ".contextwise");
|
|
532
|
+
this.syncStatePath = join5(dir, "sync.json");
|
|
533
|
+
this.localConfigPath = localConfigPath;
|
|
534
|
+
}
|
|
535
|
+
loadSyncState() {
|
|
536
|
+
if (existsSync5(this.syncStatePath)) {
|
|
537
|
+
try {
|
|
538
|
+
const raw = readFileSync5(this.syncStatePath, "utf-8");
|
|
539
|
+
return JSON.parse(raw);
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
workspaceId: "ws_default",
|
|
545
|
+
revision: 0,
|
|
546
|
+
lastSyncedAt: 0
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
saveSyncState(state) {
|
|
550
|
+
writeFileSync4(this.syncStatePath, JSON.stringify(state, null, 2), {
|
|
551
|
+
mode: 384,
|
|
552
|
+
encoding: "utf-8"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Pushes local configuration and encrypted vault payload to the cloud.
|
|
557
|
+
*/
|
|
558
|
+
async push(workspaceId, receipt) {
|
|
559
|
+
const state = this.loadSyncState();
|
|
560
|
+
const wsId = workspaceId || state.workspaceId;
|
|
561
|
+
const device = getOrCreateDeviceIdentity();
|
|
562
|
+
const localConfig = ConfigLoader.load();
|
|
563
|
+
const vaultPath = getDefaultVaultFilePath();
|
|
564
|
+
let encryptedVault;
|
|
565
|
+
if (existsSync5(vaultPath)) {
|
|
566
|
+
try {
|
|
567
|
+
const rawVault = readFileSync5(vaultPath, "utf-8");
|
|
568
|
+
encryptedVault = JSON.parse(rawVault);
|
|
569
|
+
} catch {
|
|
570
|
+
encryptedVault = this.createEmptyEncryptedVault();
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
encryptedVault = this.createEmptyEncryptedVault();
|
|
574
|
+
}
|
|
575
|
+
const payload = {
|
|
576
|
+
workspaceId: wsId,
|
|
577
|
+
deviceId: device.deviceId,
|
|
578
|
+
baseRevision: state.revision,
|
|
579
|
+
config: localConfig,
|
|
580
|
+
encryptedVault,
|
|
581
|
+
timestamp: Date.now()
|
|
582
|
+
};
|
|
583
|
+
const result = await cloudClient.pushSync(payload, receipt);
|
|
584
|
+
if (result.status === "committed") {
|
|
585
|
+
state.workspaceId = wsId;
|
|
586
|
+
state.revision = result.revision;
|
|
587
|
+
state.lastSyncedAt = Date.now();
|
|
588
|
+
this.saveSyncState(state);
|
|
589
|
+
logger.info(`Successfully pushed revision #${result.revision} to ContextWise Cloud.`);
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Pulls the latest cloud configuration and merges upstream servers.
|
|
595
|
+
*/
|
|
596
|
+
async pull(workspaceId, receipt) {
|
|
597
|
+
const state = this.loadSyncState();
|
|
598
|
+
const wsId = workspaceId || state.workspaceId;
|
|
599
|
+
const pullResult = await cloudClient.pullSync(wsId, state.revision, receipt);
|
|
600
|
+
if (!pullResult) {
|
|
601
|
+
logger.info("Local configuration is already up to date.");
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
if (pullResult.config && pullResult.config.upstreams) {
|
|
605
|
+
this.mergeConfigIntoLocal(pullResult.config);
|
|
606
|
+
}
|
|
607
|
+
if (pullResult.encryptedVault && pullResult.encryptedVault.data) {
|
|
608
|
+
const vaultPath = getDefaultVaultFilePath();
|
|
609
|
+
writeFileSync4(vaultPath, JSON.stringify(pullResult.encryptedVault, null, 2), {
|
|
610
|
+
mode: 384,
|
|
611
|
+
encoding: "utf-8"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
state.revision = pullResult.revision;
|
|
615
|
+
state.lastSyncedAt = Date.now();
|
|
616
|
+
this.saveSyncState(state);
|
|
617
|
+
logger.info(`Pulled and applied cloud revision #${pullResult.revision}.`);
|
|
618
|
+
return pullResult;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Semantically merges remote configuration into local contextwise.json.
|
|
622
|
+
*/
|
|
623
|
+
mergeConfigIntoLocal(remoteConfig) {
|
|
624
|
+
const validatedRemote = ContextWiseConfigSchema.parse(remoteConfig);
|
|
625
|
+
const configPath = this.localConfigPath || process.env.CONTEXTWISE_CONFIG || join5(process.cwd(), "contextwise.json");
|
|
626
|
+
let localConfig;
|
|
627
|
+
if (existsSync5(configPath)) {
|
|
628
|
+
try {
|
|
629
|
+
localConfig = ContextWiseConfigSchema.parse(JSON.parse(readFileSync5(configPath, "utf-8")));
|
|
630
|
+
} catch {
|
|
631
|
+
localConfig = ContextWiseConfigSchema.parse({});
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
localConfig = ContextWiseConfigSchema.parse({});
|
|
635
|
+
}
|
|
636
|
+
for (const [name, s] of Object.entries(validatedRemote.upstreams || {})) {
|
|
637
|
+
if (isStdioUpstream(s) && !localConfig.upstreams[name]) {
|
|
638
|
+
logger.warn(
|
|
639
|
+
`[Cloud Sync Security] Remote configuration contains new stdio upstream "${name}" (${s.command}). Verify this server in contextwise.json before execution.`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const mergedUpstreams = {
|
|
644
|
+
...validatedRemote.upstreams || {},
|
|
645
|
+
...localConfig.upstreams || {}
|
|
646
|
+
};
|
|
647
|
+
const merged = {
|
|
648
|
+
...localConfig,
|
|
649
|
+
upstreams: mergedUpstreams
|
|
650
|
+
};
|
|
651
|
+
writeFileSync4(configPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
652
|
+
}
|
|
653
|
+
createEmptyEncryptedVault() {
|
|
654
|
+
return {
|
|
655
|
+
version: 1,
|
|
656
|
+
kdf: {
|
|
657
|
+
algorithm: "pbkdf2-sha512",
|
|
658
|
+
salt: "empty",
|
|
659
|
+
iterations: 5e4
|
|
660
|
+
},
|
|
661
|
+
cipher: "aes-256-gcm",
|
|
662
|
+
iv: "",
|
|
663
|
+
authTag: "",
|
|
664
|
+
data: ""
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
getStatus() {
|
|
668
|
+
const token = cloudClient.getToken();
|
|
669
|
+
const state = this.loadSyncState();
|
|
670
|
+
const device = getOrCreateDeviceIdentity();
|
|
671
|
+
return {
|
|
672
|
+
isLoggedIn: cloudClient.isAuthenticated(),
|
|
673
|
+
userEmail: token?.email,
|
|
674
|
+
workspaceId: state.workspaceId,
|
|
675
|
+
revision: state.revision,
|
|
676
|
+
lastSyncedAt: state.lastSyncedAt,
|
|
677
|
+
deviceId: device.deviceId
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var syncManager = new SyncManager();
|
|
682
|
+
|
|
683
|
+
export {
|
|
684
|
+
StdioUpstreamConfigSchema,
|
|
685
|
+
HttpUpstreamConfigSchema,
|
|
686
|
+
UpstreamServerConfigSchema,
|
|
687
|
+
warnIfInsecureHttp,
|
|
688
|
+
isStdioUpstream,
|
|
689
|
+
isHttpUpstream,
|
|
690
|
+
ProxyConfigSchema,
|
|
691
|
+
RoutingConfigSchema,
|
|
692
|
+
GuardrailsConfigSchema,
|
|
693
|
+
ContextWiseConfigSchema,
|
|
694
|
+
ConfigLoader,
|
|
695
|
+
encryptAesGcm,
|
|
696
|
+
decryptAesGcm,
|
|
697
|
+
VAULT_PBKDF2_ITERATIONS,
|
|
698
|
+
deriveKeyFromPassphrase,
|
|
699
|
+
getOrCreateMachineKey,
|
|
700
|
+
generateKeyPairX25519,
|
|
701
|
+
deriveSharedSecretX25519,
|
|
702
|
+
getDefaultVaultFilePath,
|
|
703
|
+
EncryptedFileVaultDriver,
|
|
704
|
+
createEnvelope,
|
|
705
|
+
openEnvelope,
|
|
706
|
+
getOrCreateDeviceIdentity,
|
|
707
|
+
SyncManager,
|
|
708
|
+
syncManager
|
|
709
|
+
};
|
|
710
|
+
//# sourceMappingURL=chunk-OYCA37PJ.js.map
|