@i4ctime/q-ring 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/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/index.js +1525 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +1339 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +45 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
// src/mcp.ts
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
|
|
4
|
+
// src/mcp/server.ts
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/core/keyring.ts
|
|
9
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
10
|
+
|
|
11
|
+
// src/utils/hash.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
function hashProjectPath(projectPath) {
|
|
14
|
+
return createHash("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/core/scope.ts
|
|
18
|
+
var SERVICE_PREFIX = "q-ring";
|
|
19
|
+
function globalService() {
|
|
20
|
+
return `${SERVICE_PREFIX}:global`;
|
|
21
|
+
}
|
|
22
|
+
function projectService(projectPath) {
|
|
23
|
+
const hash = hashProjectPath(projectPath);
|
|
24
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
25
|
+
}
|
|
26
|
+
function resolveScope(opts2) {
|
|
27
|
+
const { scope, projectPath } = opts2;
|
|
28
|
+
if (scope === "global") {
|
|
29
|
+
return [{ scope: "global", service: globalService() }];
|
|
30
|
+
}
|
|
31
|
+
if (scope === "project") {
|
|
32
|
+
if (!projectPath) {
|
|
33
|
+
throw new Error("Project path is required for project scope");
|
|
34
|
+
}
|
|
35
|
+
return [
|
|
36
|
+
{ scope: "project", service: projectService(projectPath), projectPath }
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
if (projectPath) {
|
|
40
|
+
return [
|
|
41
|
+
{ scope: "project", service: projectService(projectPath), projectPath },
|
|
42
|
+
{ scope: "global", service: globalService() }
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
return [{ scope: "global", service: globalService() }];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/envelope.ts
|
|
49
|
+
function createEnvelope(value, opts2) {
|
|
50
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
51
|
+
let expiresAt = opts2?.expiresAt;
|
|
52
|
+
if (!expiresAt && opts2?.ttlSeconds) {
|
|
53
|
+
expiresAt = new Date(Date.now() + opts2.ttlSeconds * 1e3).toISOString();
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
v: 1,
|
|
57
|
+
value: opts2?.states ? void 0 : value,
|
|
58
|
+
states: opts2?.states,
|
|
59
|
+
defaultEnv: opts2?.defaultEnv,
|
|
60
|
+
meta: {
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
expiresAt,
|
|
64
|
+
ttlSeconds: opts2?.ttlSeconds,
|
|
65
|
+
description: opts2?.description,
|
|
66
|
+
tags: opts2?.tags,
|
|
67
|
+
entangled: opts2?.entangled,
|
|
68
|
+
accessCount: 0
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function parseEnvelope(raw) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (parsed && typeof parsed === "object" && parsed.v === 1) {
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function wrapLegacy(rawValue) {
|
|
83
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
return {
|
|
85
|
+
v: 1,
|
|
86
|
+
value: rawValue,
|
|
87
|
+
meta: {
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
accessCount: 0
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function serializeEnvelope(envelope) {
|
|
95
|
+
return JSON.stringify(envelope);
|
|
96
|
+
}
|
|
97
|
+
function collapseValue(envelope, env) {
|
|
98
|
+
if (envelope.states) {
|
|
99
|
+
const targetEnv = env ?? envelope.defaultEnv;
|
|
100
|
+
if (targetEnv && envelope.states[targetEnv]) {
|
|
101
|
+
return envelope.states[targetEnv];
|
|
102
|
+
}
|
|
103
|
+
if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
|
|
104
|
+
return envelope.states[envelope.defaultEnv];
|
|
105
|
+
}
|
|
106
|
+
const keys = Object.keys(envelope.states);
|
|
107
|
+
if (keys.length > 0) {
|
|
108
|
+
return envelope.states[keys[0]];
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return envelope.value ?? null;
|
|
113
|
+
}
|
|
114
|
+
function checkDecay(envelope) {
|
|
115
|
+
if (!envelope.meta.expiresAt) {
|
|
116
|
+
return {
|
|
117
|
+
isExpired: false,
|
|
118
|
+
isStale: false,
|
|
119
|
+
lifetimePercent: 0,
|
|
120
|
+
secondsRemaining: null,
|
|
121
|
+
timeRemaining: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
126
|
+
const created = new Date(envelope.meta.createdAt).getTime();
|
|
127
|
+
const totalLifetime = expires - created;
|
|
128
|
+
const elapsed = now - created;
|
|
129
|
+
const remaining = expires - now;
|
|
130
|
+
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
131
|
+
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
132
|
+
let timeRemaining = null;
|
|
133
|
+
if (remaining > 0) {
|
|
134
|
+
const days = Math.floor(remaining / 864e5);
|
|
135
|
+
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
136
|
+
const minutes = Math.floor(remaining % 36e5 / 6e4);
|
|
137
|
+
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
138
|
+
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
139
|
+
else timeRemaining = `${minutes}m`;
|
|
140
|
+
} else {
|
|
141
|
+
timeRemaining = "expired";
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
isExpired: remaining <= 0,
|
|
145
|
+
isStale: lifetimePercent >= 75,
|
|
146
|
+
lifetimePercent,
|
|
147
|
+
secondsRemaining,
|
|
148
|
+
timeRemaining
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function recordAccess(envelope) {
|
|
152
|
+
return {
|
|
153
|
+
...envelope,
|
|
154
|
+
meta: {
|
|
155
|
+
...envelope.meta,
|
|
156
|
+
accessCount: envelope.meta.accessCount + 1,
|
|
157
|
+
lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/core/collapse.ts
|
|
163
|
+
import { execSync } from "child_process";
|
|
164
|
+
import { existsSync, readFileSync } from "fs";
|
|
165
|
+
import { join } from "path";
|
|
166
|
+
var BRANCH_ENV_MAP = {
|
|
167
|
+
main: "prod",
|
|
168
|
+
master: "prod",
|
|
169
|
+
production: "prod",
|
|
170
|
+
develop: "dev",
|
|
171
|
+
development: "dev",
|
|
172
|
+
dev: "dev",
|
|
173
|
+
staging: "staging",
|
|
174
|
+
stage: "staging",
|
|
175
|
+
test: "test",
|
|
176
|
+
testing: "test"
|
|
177
|
+
};
|
|
178
|
+
function detectGitBranch(cwd) {
|
|
179
|
+
try {
|
|
180
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
181
|
+
cwd: cwd ?? process.cwd(),
|
|
182
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
timeout: 3e3
|
|
185
|
+
}).trim();
|
|
186
|
+
return branch || null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readProjectConfig(projectPath) {
|
|
192
|
+
const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
|
|
193
|
+
try {
|
|
194
|
+
if (existsSync(configPath)) {
|
|
195
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function collapseEnvironment(ctx = {}) {
|
|
202
|
+
if (ctx.explicit) {
|
|
203
|
+
return { env: ctx.explicit, source: "explicit" };
|
|
204
|
+
}
|
|
205
|
+
const qringEnv = process.env.QRING_ENV;
|
|
206
|
+
if (qringEnv) {
|
|
207
|
+
return { env: qringEnv, source: "QRING_ENV" };
|
|
208
|
+
}
|
|
209
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
210
|
+
if (nodeEnv) {
|
|
211
|
+
const mapped = mapEnvName(nodeEnv);
|
|
212
|
+
return { env: mapped, source: "NODE_ENV" };
|
|
213
|
+
}
|
|
214
|
+
const config = readProjectConfig(ctx.projectPath);
|
|
215
|
+
if (config?.env) {
|
|
216
|
+
return { env: config.env, source: "project-config" };
|
|
217
|
+
}
|
|
218
|
+
const branch = detectGitBranch(ctx.projectPath);
|
|
219
|
+
if (branch) {
|
|
220
|
+
const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
|
|
221
|
+
const mapped = branchMap[branch];
|
|
222
|
+
if (mapped) {
|
|
223
|
+
return { env: mapped, source: "git-branch" };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (config?.defaultEnv) {
|
|
227
|
+
return { env: config.defaultEnv, source: "project-config" };
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function mapEnvName(raw) {
|
|
232
|
+
const lower = raw.toLowerCase();
|
|
233
|
+
if (lower === "production") return "prod";
|
|
234
|
+
if (lower === "development") return "dev";
|
|
235
|
+
return lower;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/core/observer.ts
|
|
239
|
+
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2 } from "fs";
|
|
240
|
+
import { join as join2 } from "path";
|
|
241
|
+
import { homedir } from "os";
|
|
242
|
+
function getAuditDir() {
|
|
243
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
244
|
+
if (!existsSync2(dir)) {
|
|
245
|
+
mkdirSync(dir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
return dir;
|
|
248
|
+
}
|
|
249
|
+
function getAuditPath() {
|
|
250
|
+
return join2(getAuditDir(), "audit.jsonl");
|
|
251
|
+
}
|
|
252
|
+
function logAudit(event) {
|
|
253
|
+
const full = {
|
|
254
|
+
...event,
|
|
255
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
256
|
+
pid: process.pid
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function queryAudit(query = {}) {
|
|
264
|
+
const path = getAuditPath();
|
|
265
|
+
if (!existsSync2(path)) return [];
|
|
266
|
+
try {
|
|
267
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
268
|
+
let events = lines.map((line) => {
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(line);
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}).filter((e) => e !== null);
|
|
275
|
+
if (query.key) {
|
|
276
|
+
events = events.filter((e) => e.key === query.key);
|
|
277
|
+
}
|
|
278
|
+
if (query.action) {
|
|
279
|
+
events = events.filter((e) => e.action === query.action);
|
|
280
|
+
}
|
|
281
|
+
if (query.since) {
|
|
282
|
+
const since = new Date(query.since).getTime();
|
|
283
|
+
events = events.filter(
|
|
284
|
+
(e) => new Date(e.timestamp).getTime() >= since
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
events.sort(
|
|
288
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
289
|
+
);
|
|
290
|
+
if (query.limit) {
|
|
291
|
+
events = events.slice(0, query.limit);
|
|
292
|
+
}
|
|
293
|
+
return events;
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function detectAnomalies(key) {
|
|
299
|
+
const recent = queryAudit({
|
|
300
|
+
key,
|
|
301
|
+
action: "read",
|
|
302
|
+
since: new Date(Date.now() - 36e5).toISOString()
|
|
303
|
+
// last hour
|
|
304
|
+
});
|
|
305
|
+
const anomalies = [];
|
|
306
|
+
if (key && recent.length > 50) {
|
|
307
|
+
anomalies.push({
|
|
308
|
+
type: "burst",
|
|
309
|
+
description: `${recent.length} reads of "${key}" in the last hour`,
|
|
310
|
+
events: recent.slice(0, 10)
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const nightAccess = recent.filter((e) => {
|
|
314
|
+
const hour = new Date(e.timestamp).getHours();
|
|
315
|
+
return hour >= 1 && hour < 5;
|
|
316
|
+
});
|
|
317
|
+
if (nightAccess.length > 0) {
|
|
318
|
+
anomalies.push({
|
|
319
|
+
type: "unusual-hour",
|
|
320
|
+
description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
|
|
321
|
+
events: nightAccess
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return anomalies;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/core/entanglement.ts
|
|
328
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
329
|
+
import { join as join3 } from "path";
|
|
330
|
+
import { homedir as homedir2 } from "os";
|
|
331
|
+
function getRegistryPath() {
|
|
332
|
+
const dir = join3(homedir2(), ".config", "q-ring");
|
|
333
|
+
if (!existsSync3(dir)) {
|
|
334
|
+
mkdirSync2(dir, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
return join3(dir, "entanglement.json");
|
|
337
|
+
}
|
|
338
|
+
function loadRegistry() {
|
|
339
|
+
const path = getRegistryPath();
|
|
340
|
+
if (!existsSync3(path)) {
|
|
341
|
+
return { pairs: [] };
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
345
|
+
} catch {
|
|
346
|
+
return { pairs: [] };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function saveRegistry(registry) {
|
|
350
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));
|
|
351
|
+
}
|
|
352
|
+
function entangle(source, target) {
|
|
353
|
+
const registry = loadRegistry();
|
|
354
|
+
const exists = registry.pairs.some(
|
|
355
|
+
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
356
|
+
);
|
|
357
|
+
if (!exists) {
|
|
358
|
+
registry.pairs.push({
|
|
359
|
+
source,
|
|
360
|
+
target,
|
|
361
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
362
|
+
});
|
|
363
|
+
registry.pairs.push({
|
|
364
|
+
source: target,
|
|
365
|
+
target: source,
|
|
366
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
367
|
+
});
|
|
368
|
+
saveRegistry(registry);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function findEntangled(source) {
|
|
372
|
+
const registry = loadRegistry();
|
|
373
|
+
return registry.pairs.filter(
|
|
374
|
+
(p) => p.source.service === source.service && p.source.key === source.key
|
|
375
|
+
).map((p) => p.target);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/core/keyring.ts
|
|
379
|
+
function readEnvelope(service, key) {
|
|
380
|
+
const entry = new Entry(service, key);
|
|
381
|
+
const raw = entry.getPassword();
|
|
382
|
+
if (raw === null) return null;
|
|
383
|
+
const envelope = parseEnvelope(raw);
|
|
384
|
+
return envelope ?? wrapLegacy(raw);
|
|
385
|
+
}
|
|
386
|
+
function writeEnvelope(service, key, envelope) {
|
|
387
|
+
const entry = new Entry(service, key);
|
|
388
|
+
entry.setPassword(serializeEnvelope(envelope));
|
|
389
|
+
}
|
|
390
|
+
function resolveEnv(opts2) {
|
|
391
|
+
if (opts2.env) return opts2.env;
|
|
392
|
+
const result = collapseEnvironment({ projectPath: opts2.projectPath });
|
|
393
|
+
return result?.env;
|
|
394
|
+
}
|
|
395
|
+
function getSecret(key, opts2 = {}) {
|
|
396
|
+
const scopes = resolveScope(opts2);
|
|
397
|
+
const env = resolveEnv(opts2);
|
|
398
|
+
const source = opts2.source ?? "cli";
|
|
399
|
+
for (const { service, scope } of scopes) {
|
|
400
|
+
const envelope = readEnvelope(service, key);
|
|
401
|
+
if (!envelope) continue;
|
|
402
|
+
const decay = checkDecay(envelope);
|
|
403
|
+
if (decay.isExpired) {
|
|
404
|
+
logAudit({
|
|
405
|
+
action: "read",
|
|
406
|
+
key,
|
|
407
|
+
scope,
|
|
408
|
+
source,
|
|
409
|
+
detail: "blocked: secret expired (quantum decay)"
|
|
410
|
+
});
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const value = collapseValue(envelope, env);
|
|
414
|
+
if (value === null) continue;
|
|
415
|
+
const updated = recordAccess(envelope);
|
|
416
|
+
writeEnvelope(service, key, updated);
|
|
417
|
+
logAudit({ action: "read", key, scope, env, source });
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function getEnvelope(key, opts2 = {}) {
|
|
423
|
+
const scopes = resolveScope(opts2);
|
|
424
|
+
for (const { service, scope } of scopes) {
|
|
425
|
+
const envelope = readEnvelope(service, key);
|
|
426
|
+
if (envelope) return { envelope, scope };
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function setSecret(key, value, opts2 = {}) {
|
|
431
|
+
const scope = opts2.scope ?? "global";
|
|
432
|
+
const scopes = resolveScope({ ...opts2, scope });
|
|
433
|
+
const { service } = scopes[0];
|
|
434
|
+
const source = opts2.source ?? "cli";
|
|
435
|
+
const existing = readEnvelope(service, key);
|
|
436
|
+
let envelope;
|
|
437
|
+
if (opts2.states) {
|
|
438
|
+
envelope = createEnvelope("", {
|
|
439
|
+
states: opts2.states,
|
|
440
|
+
defaultEnv: opts2.defaultEnv,
|
|
441
|
+
description: opts2.description,
|
|
442
|
+
tags: opts2.tags,
|
|
443
|
+
ttlSeconds: opts2.ttlSeconds,
|
|
444
|
+
expiresAt: opts2.expiresAt,
|
|
445
|
+
entangled: existing?.meta.entangled
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
envelope = createEnvelope(value, {
|
|
449
|
+
description: opts2.description,
|
|
450
|
+
tags: opts2.tags,
|
|
451
|
+
ttlSeconds: opts2.ttlSeconds,
|
|
452
|
+
expiresAt: opts2.expiresAt,
|
|
453
|
+
entangled: existing?.meta.entangled
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (existing) {
|
|
457
|
+
envelope.meta.createdAt = existing.meta.createdAt;
|
|
458
|
+
envelope.meta.accessCount = existing.meta.accessCount;
|
|
459
|
+
}
|
|
460
|
+
writeEnvelope(service, key, envelope);
|
|
461
|
+
logAudit({ action: "write", key, scope, source });
|
|
462
|
+
const entangled = findEntangled({ service, key });
|
|
463
|
+
for (const target of entangled) {
|
|
464
|
+
try {
|
|
465
|
+
const targetEnvelope = readEnvelope(target.service, target.key);
|
|
466
|
+
if (targetEnvelope) {
|
|
467
|
+
if (opts2.states) {
|
|
468
|
+
targetEnvelope.states = opts2.states;
|
|
469
|
+
} else {
|
|
470
|
+
targetEnvelope.value = value;
|
|
471
|
+
}
|
|
472
|
+
targetEnvelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
473
|
+
writeEnvelope(target.service, target.key, targetEnvelope);
|
|
474
|
+
logAudit({
|
|
475
|
+
action: "entangle",
|
|
476
|
+
key: target.key,
|
|
477
|
+
scope: "global",
|
|
478
|
+
source,
|
|
479
|
+
detail: `propagated from ${key}`
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function deleteSecret(key, opts2 = {}) {
|
|
487
|
+
const scopes = resolveScope(opts2);
|
|
488
|
+
const source = opts2.source ?? "cli";
|
|
489
|
+
let deleted = false;
|
|
490
|
+
for (const { service, scope } of scopes) {
|
|
491
|
+
const entry = new Entry(service, key);
|
|
492
|
+
try {
|
|
493
|
+
if (entry.deleteCredential()) {
|
|
494
|
+
deleted = true;
|
|
495
|
+
logAudit({ action: "delete", key, scope, source });
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return deleted;
|
|
501
|
+
}
|
|
502
|
+
function hasSecret(key, opts2 = {}) {
|
|
503
|
+
const scopes = resolveScope(opts2);
|
|
504
|
+
for (const { service } of scopes) {
|
|
505
|
+
const envelope = readEnvelope(service, key);
|
|
506
|
+
if (envelope) {
|
|
507
|
+
const decay = checkDecay(envelope);
|
|
508
|
+
if (!decay.isExpired) return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
function listSecrets(opts2 = {}) {
|
|
514
|
+
const source = opts2.source ?? "cli";
|
|
515
|
+
const services = [];
|
|
516
|
+
if (!opts2.scope || opts2.scope === "global") {
|
|
517
|
+
services.push({ service: globalService(), scope: "global" });
|
|
518
|
+
}
|
|
519
|
+
if ((!opts2.scope || opts2.scope === "project") && opts2.projectPath) {
|
|
520
|
+
services.push({
|
|
521
|
+
service: projectService(opts2.projectPath),
|
|
522
|
+
scope: "project"
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const results = [];
|
|
526
|
+
const seen = /* @__PURE__ */ new Set();
|
|
527
|
+
for (const { service, scope } of services) {
|
|
528
|
+
try {
|
|
529
|
+
const credentials = findCredentials(service);
|
|
530
|
+
for (const cred of credentials) {
|
|
531
|
+
const id = `${scope}:${cred.account}`;
|
|
532
|
+
if (seen.has(id)) continue;
|
|
533
|
+
seen.add(id);
|
|
534
|
+
const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
|
|
535
|
+
const decay = checkDecay(envelope);
|
|
536
|
+
results.push({
|
|
537
|
+
key: cred.account,
|
|
538
|
+
scope,
|
|
539
|
+
envelope,
|
|
540
|
+
decay
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
logAudit({ action: "list", source });
|
|
547
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
548
|
+
}
|
|
549
|
+
function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
550
|
+
const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
|
|
551
|
+
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
552
|
+
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
553
|
+
const target = { service: targetScopes[0].service, key: targetKey };
|
|
554
|
+
entangle(source, target);
|
|
555
|
+
logAudit({
|
|
556
|
+
action: "entangle",
|
|
557
|
+
key: sourceKey,
|
|
558
|
+
source: sourceOpts.source ?? "cli",
|
|
559
|
+
detail: `entangled with ${targetKey}`
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/core/noise.ts
|
|
564
|
+
import { randomBytes, randomInt } from "crypto";
|
|
565
|
+
var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
566
|
+
var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
567
|
+
function randomString(charset, length) {
|
|
568
|
+
let result = "";
|
|
569
|
+
for (let i = 0; i < length; i++) {
|
|
570
|
+
result += charset[randomInt(charset.length)];
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
function generateSecret(opts2 = {}) {
|
|
575
|
+
const format = opts2.format ?? "api-key";
|
|
576
|
+
switch (format) {
|
|
577
|
+
case "hex": {
|
|
578
|
+
const len = opts2.length ?? 32;
|
|
579
|
+
return randomBytes(len).toString("hex");
|
|
580
|
+
}
|
|
581
|
+
case "base64": {
|
|
582
|
+
const len = opts2.length ?? 32;
|
|
583
|
+
return randomBytes(len).toString("base64url");
|
|
584
|
+
}
|
|
585
|
+
case "alphanumeric": {
|
|
586
|
+
const len = opts2.length ?? 32;
|
|
587
|
+
return randomString(ALPHA_NUM, len);
|
|
588
|
+
}
|
|
589
|
+
case "uuid": {
|
|
590
|
+
const bytes = randomBytes(16);
|
|
591
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
592
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
593
|
+
const hex = bytes.toString("hex");
|
|
594
|
+
return [
|
|
595
|
+
hex.slice(0, 8),
|
|
596
|
+
hex.slice(8, 12),
|
|
597
|
+
hex.slice(12, 16),
|
|
598
|
+
hex.slice(16, 20),
|
|
599
|
+
hex.slice(20, 32)
|
|
600
|
+
].join("-");
|
|
601
|
+
}
|
|
602
|
+
case "api-key": {
|
|
603
|
+
const prefix = opts2.prefix ?? "qr_";
|
|
604
|
+
const len = opts2.length ?? 48;
|
|
605
|
+
return prefix + randomString(ALPHA_NUM, len);
|
|
606
|
+
}
|
|
607
|
+
case "token": {
|
|
608
|
+
const prefix = opts2.prefix ?? "";
|
|
609
|
+
const len = opts2.length ?? 64;
|
|
610
|
+
return prefix + randomBytes(len).toString("base64url");
|
|
611
|
+
}
|
|
612
|
+
case "password": {
|
|
613
|
+
const len = opts2.length ?? 24;
|
|
614
|
+
let pw = randomString(PASSWORD_CHARS, len);
|
|
615
|
+
const hasUpper = /[A-Z]/.test(pw);
|
|
616
|
+
const hasLower = /[a-z]/.test(pw);
|
|
617
|
+
const hasDigit = /[0-9]/.test(pw);
|
|
618
|
+
const hasSpecial = /[^A-Za-z0-9]/.test(pw);
|
|
619
|
+
if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
|
|
620
|
+
if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
|
|
621
|
+
if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
|
|
622
|
+
if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
|
|
623
|
+
return pw;
|
|
624
|
+
}
|
|
625
|
+
default:
|
|
626
|
+
return randomBytes(32).toString("hex");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function replaceAt(str, index, char) {
|
|
630
|
+
return str.slice(0, index) + char + str.slice(index + 1);
|
|
631
|
+
}
|
|
632
|
+
function estimateEntropy(secret) {
|
|
633
|
+
const charsets = [
|
|
634
|
+
{ regex: /[a-z]/, size: 26 },
|
|
635
|
+
{ regex: /[A-Z]/, size: 26 },
|
|
636
|
+
{ regex: /[0-9]/, size: 10 },
|
|
637
|
+
{ regex: /[^A-Za-z0-9]/, size: 32 }
|
|
638
|
+
];
|
|
639
|
+
let poolSize = 0;
|
|
640
|
+
for (const { regex, size } of charsets) {
|
|
641
|
+
if (regex.test(secret)) poolSize += size;
|
|
642
|
+
}
|
|
643
|
+
return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/utils/colors.ts
|
|
647
|
+
var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
648
|
+
|
|
649
|
+
// src/core/agent.ts
|
|
650
|
+
function defaultConfig() {
|
|
651
|
+
return {
|
|
652
|
+
intervalSeconds: 60,
|
|
653
|
+
autoRotate: false,
|
|
654
|
+
projectPaths: [process.cwd()],
|
|
655
|
+
verbose: false
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function runHealthScan(config = {}) {
|
|
659
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
660
|
+
const report = {
|
|
661
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
662
|
+
totalSecrets: 0,
|
|
663
|
+
healthy: 0,
|
|
664
|
+
stale: 0,
|
|
665
|
+
expired: 0,
|
|
666
|
+
anomalies: 0,
|
|
667
|
+
rotated: [],
|
|
668
|
+
warnings: []
|
|
669
|
+
};
|
|
670
|
+
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
671
|
+
const projectEntries = cfg.projectPaths.flatMap(
|
|
672
|
+
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
673
|
+
);
|
|
674
|
+
const allEntries = [...globalEntries, ...projectEntries];
|
|
675
|
+
report.totalSecrets = allEntries.length;
|
|
676
|
+
for (const entry of allEntries) {
|
|
677
|
+
if (!entry.envelope) continue;
|
|
678
|
+
const decay = checkDecay(entry.envelope);
|
|
679
|
+
if (decay.isExpired) {
|
|
680
|
+
report.expired++;
|
|
681
|
+
report.warnings.push(
|
|
682
|
+
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
683
|
+
);
|
|
684
|
+
if (cfg.autoRotate) {
|
|
685
|
+
const newValue = generateSecret({ format: "api-key" });
|
|
686
|
+
setSecret(entry.key, newValue, {
|
|
687
|
+
scope: entry.scope,
|
|
688
|
+
projectPath: cfg.projectPaths[0],
|
|
689
|
+
source: "agent"
|
|
690
|
+
});
|
|
691
|
+
report.rotated.push(entry.key);
|
|
692
|
+
logAudit({
|
|
693
|
+
action: "write",
|
|
694
|
+
key: entry.key,
|
|
695
|
+
scope: entry.scope,
|
|
696
|
+
source: "agent",
|
|
697
|
+
detail: "auto-rotated by agent (expired)"
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
} else if (decay.isStale) {
|
|
701
|
+
report.stale++;
|
|
702
|
+
report.warnings.push(
|
|
703
|
+
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
704
|
+
);
|
|
705
|
+
} else {
|
|
706
|
+
report.healthy++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const anomalies = detectAnomalies();
|
|
710
|
+
report.anomalies = anomalies.length;
|
|
711
|
+
for (const a of anomalies) {
|
|
712
|
+
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
713
|
+
}
|
|
714
|
+
return report;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/core/tunnel.ts
|
|
718
|
+
var tunnelStore = /* @__PURE__ */ new Map();
|
|
719
|
+
var cleanupInterval = null;
|
|
720
|
+
function ensureCleanup() {
|
|
721
|
+
if (cleanupInterval) return;
|
|
722
|
+
cleanupInterval = setInterval(() => {
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
for (const [id, entry] of tunnelStore) {
|
|
725
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
726
|
+
tunnelStore.delete(id);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (tunnelStore.size === 0 && cleanupInterval) {
|
|
730
|
+
clearInterval(cleanupInterval);
|
|
731
|
+
cleanupInterval = null;
|
|
732
|
+
}
|
|
733
|
+
}, 5e3);
|
|
734
|
+
if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
|
|
735
|
+
cleanupInterval.unref();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function tunnelCreate(value, opts2 = {}) {
|
|
739
|
+
const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
740
|
+
const now = Date.now();
|
|
741
|
+
tunnelStore.set(id, {
|
|
742
|
+
value,
|
|
743
|
+
createdAt: now,
|
|
744
|
+
expiresAt: opts2.ttlSeconds ? now + opts2.ttlSeconds * 1e3 : void 0,
|
|
745
|
+
accessCount: 0,
|
|
746
|
+
maxReads: opts2.maxReads
|
|
747
|
+
});
|
|
748
|
+
ensureCleanup();
|
|
749
|
+
return id;
|
|
750
|
+
}
|
|
751
|
+
function tunnelRead(id) {
|
|
752
|
+
const entry = tunnelStore.get(id);
|
|
753
|
+
if (!entry) return null;
|
|
754
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
755
|
+
tunnelStore.delete(id);
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
entry.accessCount++;
|
|
759
|
+
if (entry.maxReads && entry.accessCount >= entry.maxReads) {
|
|
760
|
+
const value = entry.value;
|
|
761
|
+
tunnelStore.delete(id);
|
|
762
|
+
return value;
|
|
763
|
+
}
|
|
764
|
+
return entry.value;
|
|
765
|
+
}
|
|
766
|
+
function tunnelDestroy(id) {
|
|
767
|
+
return tunnelStore.delete(id);
|
|
768
|
+
}
|
|
769
|
+
function tunnelList() {
|
|
770
|
+
const now = Date.now();
|
|
771
|
+
const result = [];
|
|
772
|
+
for (const [id, entry] of tunnelStore) {
|
|
773
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
774
|
+
tunnelStore.delete(id);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
result.push({
|
|
778
|
+
id,
|
|
779
|
+
createdAt: entry.createdAt,
|
|
780
|
+
expiresAt: entry.expiresAt,
|
|
781
|
+
accessCount: entry.accessCount,
|
|
782
|
+
maxReads: entry.maxReads
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/core/teleport.ts
|
|
789
|
+
import {
|
|
790
|
+
randomBytes as randomBytes2,
|
|
791
|
+
createCipheriv,
|
|
792
|
+
createDecipheriv,
|
|
793
|
+
pbkdf2Sync
|
|
794
|
+
} from "crypto";
|
|
795
|
+
var ALGORITHM = "aes-256-gcm";
|
|
796
|
+
var KEY_LENGTH = 32;
|
|
797
|
+
var IV_LENGTH = 16;
|
|
798
|
+
var SALT_LENGTH = 32;
|
|
799
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
800
|
+
function deriveKey(passphrase, salt) {
|
|
801
|
+
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
802
|
+
}
|
|
803
|
+
function teleportPack(secrets, passphrase) {
|
|
804
|
+
const payload = {
|
|
805
|
+
secrets,
|
|
806
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
807
|
+
};
|
|
808
|
+
const plaintext = JSON.stringify(payload);
|
|
809
|
+
const salt = randomBytes2(SALT_LENGTH);
|
|
810
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
811
|
+
const key = deriveKey(passphrase, salt);
|
|
812
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
813
|
+
const encrypted = Buffer.concat([
|
|
814
|
+
cipher.update(plaintext, "utf8"),
|
|
815
|
+
cipher.final()
|
|
816
|
+
]);
|
|
817
|
+
const tag = cipher.getAuthTag();
|
|
818
|
+
const bundle = {
|
|
819
|
+
v: 1,
|
|
820
|
+
data: encrypted.toString("base64"),
|
|
821
|
+
salt: salt.toString("base64"),
|
|
822
|
+
iv: iv.toString("base64"),
|
|
823
|
+
tag: tag.toString("base64"),
|
|
824
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
825
|
+
count: secrets.length
|
|
826
|
+
};
|
|
827
|
+
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
828
|
+
}
|
|
829
|
+
function teleportUnpack(encoded, passphrase) {
|
|
830
|
+
const bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
831
|
+
const bundle = JSON.parse(bundleJson);
|
|
832
|
+
if (bundle.v !== 1) {
|
|
833
|
+
throw new Error(`Unsupported teleport bundle version: ${bundle.v}`);
|
|
834
|
+
}
|
|
835
|
+
const salt = Buffer.from(bundle.salt, "base64");
|
|
836
|
+
const iv = Buffer.from(bundle.iv, "base64");
|
|
837
|
+
const tag = Buffer.from(bundle.tag, "base64");
|
|
838
|
+
const encrypted = Buffer.from(bundle.data, "base64");
|
|
839
|
+
const key = deriveKey(passphrase, salt);
|
|
840
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
841
|
+
decipher.setAuthTag(tag);
|
|
842
|
+
const decrypted = Buffer.concat([
|
|
843
|
+
decipher.update(encrypted),
|
|
844
|
+
decipher.final()
|
|
845
|
+
]);
|
|
846
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/mcp/server.ts
|
|
850
|
+
function text(t, isError = false) {
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: t }],
|
|
853
|
+
...isError ? { isError: true } : {}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function opts(params) {
|
|
857
|
+
return {
|
|
858
|
+
scope: params.scope,
|
|
859
|
+
projectPath: params.projectPath ?? process.cwd(),
|
|
860
|
+
env: params.env,
|
|
861
|
+
source: "mcp"
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function createMcpServer() {
|
|
865
|
+
const server2 = new McpServer({
|
|
866
|
+
name: "q-ring",
|
|
867
|
+
version: "0.2.0"
|
|
868
|
+
});
|
|
869
|
+
const scopeSchema = z.enum(["global", "project"]).optional().describe("Scope: global or project");
|
|
870
|
+
const projectPathSchema = z.string().optional().describe("Project root path for project-scoped secrets");
|
|
871
|
+
const envSchema = z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)");
|
|
872
|
+
server2.tool(
|
|
873
|
+
"get_secret",
|
|
874
|
+
"Retrieve a secret by key. Collapses superposition if the secret has multiple environment states. Records access in audit log (observer effect).",
|
|
875
|
+
{
|
|
876
|
+
key: z.string().describe("The secret key name"),
|
|
877
|
+
scope: scopeSchema,
|
|
878
|
+
projectPath: projectPathSchema,
|
|
879
|
+
env: envSchema
|
|
880
|
+
},
|
|
881
|
+
async (params) => {
|
|
882
|
+
const value = getSecret(params.key, opts(params));
|
|
883
|
+
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
884
|
+
return text(value);
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
server2.tool(
|
|
888
|
+
"list_secrets",
|
|
889
|
+
"List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed.",
|
|
890
|
+
{
|
|
891
|
+
scope: scopeSchema,
|
|
892
|
+
projectPath: projectPathSchema
|
|
893
|
+
},
|
|
894
|
+
async (params) => {
|
|
895
|
+
const entries = listSecrets(opts(params));
|
|
896
|
+
if (entries.length === 0) return text("No secrets found");
|
|
897
|
+
const lines = entries.map((e) => {
|
|
898
|
+
const parts = [`[${e.scope}] ${e.key}`];
|
|
899
|
+
if (e.envelope?.states) {
|
|
900
|
+
parts.push(`states:[${Object.keys(e.envelope.states).join(",")}]`);
|
|
901
|
+
}
|
|
902
|
+
if (e.decay?.isExpired) {
|
|
903
|
+
parts.push("EXPIRED");
|
|
904
|
+
} else if (e.decay?.isStale) {
|
|
905
|
+
parts.push(`stale(${e.decay.lifetimePercent}%)`);
|
|
906
|
+
}
|
|
907
|
+
if (e.decay?.timeRemaining && !e.decay.isExpired) {
|
|
908
|
+
parts.push(`ttl:${e.decay.timeRemaining}`);
|
|
909
|
+
}
|
|
910
|
+
if (e.envelope?.meta.entangled?.length) {
|
|
911
|
+
parts.push(`entangled:${e.envelope.meta.entangled.length}`);
|
|
912
|
+
}
|
|
913
|
+
if (e.envelope && e.envelope.meta.accessCount > 0) {
|
|
914
|
+
parts.push(`reads:${e.envelope.meta.accessCount}`);
|
|
915
|
+
}
|
|
916
|
+
return parts.join(" | ");
|
|
917
|
+
});
|
|
918
|
+
return text(lines.join("\n"));
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
server2.tool(
|
|
922
|
+
"set_secret",
|
|
923
|
+
"Store a secret with optional quantum metadata: TTL (decay), environment state (superposition), description, tags.",
|
|
924
|
+
{
|
|
925
|
+
key: z.string().describe("The secret key name"),
|
|
926
|
+
value: z.string().describe("The secret value"),
|
|
927
|
+
scope: scopeSchema.default("global"),
|
|
928
|
+
projectPath: projectPathSchema,
|
|
929
|
+
env: z.string().optional().describe("If provided, sets the value for this specific environment (superposition)"),
|
|
930
|
+
ttlSeconds: z.number().optional().describe("Time-to-live in seconds (quantum decay)"),
|
|
931
|
+
description: z.string().optional().describe("Human-readable description"),
|
|
932
|
+
tags: z.array(z.string()).optional().describe("Tags for organization")
|
|
933
|
+
},
|
|
934
|
+
async (params) => {
|
|
935
|
+
const o = opts(params);
|
|
936
|
+
if (params.env) {
|
|
937
|
+
const existing = getEnvelope(params.key, o);
|
|
938
|
+
const states = existing?.envelope?.states ?? {};
|
|
939
|
+
states[params.env] = params.value;
|
|
940
|
+
if (existing?.envelope?.value && !states["default"]) {
|
|
941
|
+
states["default"] = existing.envelope.value;
|
|
942
|
+
}
|
|
943
|
+
setSecret(params.key, "", {
|
|
944
|
+
...o,
|
|
945
|
+
states,
|
|
946
|
+
defaultEnv: existing?.envelope?.defaultEnv ?? params.env,
|
|
947
|
+
ttlSeconds: params.ttlSeconds,
|
|
948
|
+
description: params.description,
|
|
949
|
+
tags: params.tags
|
|
950
|
+
});
|
|
951
|
+
return text(`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`);
|
|
952
|
+
}
|
|
953
|
+
setSecret(params.key, params.value, {
|
|
954
|
+
...o,
|
|
955
|
+
ttlSeconds: params.ttlSeconds,
|
|
956
|
+
description: params.description,
|
|
957
|
+
tags: params.tags
|
|
958
|
+
});
|
|
959
|
+
return text(`[${params.scope ?? "global"}] ${params.key} saved`);
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
server2.tool(
|
|
963
|
+
"delete_secret",
|
|
964
|
+
"Remove a secret from the keyring.",
|
|
965
|
+
{
|
|
966
|
+
key: z.string().describe("The secret key name"),
|
|
967
|
+
scope: scopeSchema,
|
|
968
|
+
projectPath: projectPathSchema
|
|
969
|
+
},
|
|
970
|
+
async (params) => {
|
|
971
|
+
const deleted = deleteSecret(params.key, opts(params));
|
|
972
|
+
return text(
|
|
973
|
+
deleted ? `Deleted "${params.key}"` : `Secret "${params.key}" not found`,
|
|
974
|
+
!deleted
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
server2.tool(
|
|
979
|
+
"has_secret",
|
|
980
|
+
"Check if a secret exists. Returns boolean. Never reveals the value. Respects decay \u2014 expired secrets return false.",
|
|
981
|
+
{
|
|
982
|
+
key: z.string().describe("The secret key name"),
|
|
983
|
+
scope: scopeSchema,
|
|
984
|
+
projectPath: projectPathSchema
|
|
985
|
+
},
|
|
986
|
+
async (params) => {
|
|
987
|
+
return text(hasSecret(params.key, opts(params)) ? "true" : "false");
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
server2.tool(
|
|
991
|
+
"inspect_secret",
|
|
992
|
+
"Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.",
|
|
993
|
+
{
|
|
994
|
+
key: z.string().describe("The secret key name"),
|
|
995
|
+
scope: scopeSchema,
|
|
996
|
+
projectPath: projectPathSchema
|
|
997
|
+
},
|
|
998
|
+
async (params) => {
|
|
999
|
+
const result = getEnvelope(params.key, opts(params));
|
|
1000
|
+
if (!result) return text(`Secret "${params.key}" not found`, true);
|
|
1001
|
+
const { envelope, scope } = result;
|
|
1002
|
+
const decay = checkDecay(envelope);
|
|
1003
|
+
const info = {
|
|
1004
|
+
key: params.key,
|
|
1005
|
+
scope,
|
|
1006
|
+
type: envelope.states ? "superposition" : "collapsed",
|
|
1007
|
+
created: envelope.meta.createdAt,
|
|
1008
|
+
updated: envelope.meta.updatedAt,
|
|
1009
|
+
accessCount: envelope.meta.accessCount,
|
|
1010
|
+
lastAccessed: envelope.meta.lastAccessedAt ?? "never"
|
|
1011
|
+
};
|
|
1012
|
+
if (envelope.states) {
|
|
1013
|
+
info.environments = Object.keys(envelope.states);
|
|
1014
|
+
info.defaultEnv = envelope.defaultEnv;
|
|
1015
|
+
}
|
|
1016
|
+
if (decay.timeRemaining) {
|
|
1017
|
+
info.decay = {
|
|
1018
|
+
expired: decay.isExpired,
|
|
1019
|
+
stale: decay.isStale,
|
|
1020
|
+
lifetimePercent: decay.lifetimePercent,
|
|
1021
|
+
timeRemaining: decay.timeRemaining
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
if (envelope.meta.entangled?.length) {
|
|
1025
|
+
info.entangled = envelope.meta.entangled;
|
|
1026
|
+
}
|
|
1027
|
+
if (envelope.meta.description) info.description = envelope.meta.description;
|
|
1028
|
+
if (envelope.meta.tags?.length) info.tags = envelope.meta.tags;
|
|
1029
|
+
return text(JSON.stringify(info, null, 2));
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
server2.tool(
|
|
1033
|
+
"detect_environment",
|
|
1034
|
+
"Detect the current environment context (wavefunction collapse). Returns the detected environment and its source (NODE_ENV, git branch, project config, etc.).",
|
|
1035
|
+
{
|
|
1036
|
+
projectPath: projectPathSchema
|
|
1037
|
+
},
|
|
1038
|
+
async (params) => {
|
|
1039
|
+
const result = collapseEnvironment({
|
|
1040
|
+
projectPath: params.projectPath ?? process.cwd()
|
|
1041
|
+
});
|
|
1042
|
+
if (!result) {
|
|
1043
|
+
return text(
|
|
1044
|
+
"No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json"
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
return text(JSON.stringify(result, null, 2));
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
server2.tool(
|
|
1051
|
+
"generate_secret",
|
|
1052
|
+
"Generate a cryptographic secret (quantum noise). Formats: hex, base64, alphanumeric, uuid, api-key, token, password. Optionally save directly to the keyring.",
|
|
1053
|
+
{
|
|
1054
|
+
format: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().default("api-key").describe("Output format"),
|
|
1055
|
+
length: z.number().optional().describe("Length in bytes or characters"),
|
|
1056
|
+
prefix: z.string().optional().describe("Prefix for api-key/token format"),
|
|
1057
|
+
saveAs: z.string().optional().describe("If provided, save the generated secret with this key name"),
|
|
1058
|
+
scope: scopeSchema.default("global"),
|
|
1059
|
+
projectPath: projectPathSchema
|
|
1060
|
+
},
|
|
1061
|
+
async (params) => {
|
|
1062
|
+
const secret = generateSecret({
|
|
1063
|
+
format: params.format,
|
|
1064
|
+
length: params.length,
|
|
1065
|
+
prefix: params.prefix
|
|
1066
|
+
});
|
|
1067
|
+
if (params.saveAs) {
|
|
1068
|
+
setSecret(params.saveAs, secret, {
|
|
1069
|
+
...opts(params),
|
|
1070
|
+
description: `Generated ${params.format} secret`
|
|
1071
|
+
});
|
|
1072
|
+
const entropy = estimateEntropy(secret);
|
|
1073
|
+
return text(
|
|
1074
|
+
`Generated and saved as "${params.saveAs}" (${params.format}, ~${entropy} bits entropy)`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return text(secret);
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
server2.tool(
|
|
1081
|
+
"entangle_secrets",
|
|
1082
|
+
"Create a quantum entanglement between two secrets. When the source is rotated/updated, the target automatically receives the same value.",
|
|
1083
|
+
{
|
|
1084
|
+
sourceKey: z.string().describe("Source secret key"),
|
|
1085
|
+
targetKey: z.string().describe("Target secret key"),
|
|
1086
|
+
sourceScope: scopeSchema.default("global"),
|
|
1087
|
+
targetScope: scopeSchema.default("global"),
|
|
1088
|
+
sourceProjectPath: z.string().optional(),
|
|
1089
|
+
targetProjectPath: z.string().optional()
|
|
1090
|
+
},
|
|
1091
|
+
async (params) => {
|
|
1092
|
+
entangleSecrets(
|
|
1093
|
+
params.sourceKey,
|
|
1094
|
+
{
|
|
1095
|
+
scope: params.sourceScope,
|
|
1096
|
+
projectPath: params.sourceProjectPath ?? process.cwd(),
|
|
1097
|
+
source: "mcp"
|
|
1098
|
+
},
|
|
1099
|
+
params.targetKey,
|
|
1100
|
+
{
|
|
1101
|
+
scope: params.targetScope,
|
|
1102
|
+
projectPath: params.targetProjectPath ?? process.cwd(),
|
|
1103
|
+
source: "mcp"
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1106
|
+
return text(`Entangled: ${params.sourceKey} <-> ${params.targetKey}`);
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
server2.tool(
|
|
1110
|
+
"tunnel_create",
|
|
1111
|
+
"Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.",
|
|
1112
|
+
{
|
|
1113
|
+
value: z.string().describe("The secret value"),
|
|
1114
|
+
ttlSeconds: z.number().optional().describe("Auto-expire after N seconds"),
|
|
1115
|
+
maxReads: z.number().optional().describe("Self-destruct after N reads")
|
|
1116
|
+
},
|
|
1117
|
+
async (params) => {
|
|
1118
|
+
const id = tunnelCreate(params.value, {
|
|
1119
|
+
ttlSeconds: params.ttlSeconds,
|
|
1120
|
+
maxReads: params.maxReads
|
|
1121
|
+
});
|
|
1122
|
+
return text(id);
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
server2.tool(
|
|
1126
|
+
"tunnel_read",
|
|
1127
|
+
"Read an ephemeral tunneled secret by ID. May self-destruct if max-reads is reached.",
|
|
1128
|
+
{
|
|
1129
|
+
id: z.string().describe("Tunnel ID")
|
|
1130
|
+
},
|
|
1131
|
+
async (params) => {
|
|
1132
|
+
const value = tunnelRead(params.id);
|
|
1133
|
+
if (value === null) {
|
|
1134
|
+
return text(`Tunnel "${params.id}" not found or expired`, true);
|
|
1135
|
+
}
|
|
1136
|
+
return text(value);
|
|
1137
|
+
}
|
|
1138
|
+
);
|
|
1139
|
+
server2.tool(
|
|
1140
|
+
"tunnel_list",
|
|
1141
|
+
"List active tunneled secrets (IDs and metadata only, never values).",
|
|
1142
|
+
{},
|
|
1143
|
+
async () => {
|
|
1144
|
+
const tunnels = tunnelList();
|
|
1145
|
+
if (tunnels.length === 0) return text("No active tunnels");
|
|
1146
|
+
const lines = tunnels.map((t) => {
|
|
1147
|
+
const parts = [t.id];
|
|
1148
|
+
parts.push(`reads:${t.accessCount}`);
|
|
1149
|
+
if (t.maxReads) parts.push(`max:${t.maxReads}`);
|
|
1150
|
+
if (t.expiresAt) {
|
|
1151
|
+
const rem = Math.max(0, Math.floor((t.expiresAt - Date.now()) / 1e3));
|
|
1152
|
+
parts.push(`expires:${rem}s`);
|
|
1153
|
+
}
|
|
1154
|
+
return parts.join(" | ");
|
|
1155
|
+
});
|
|
1156
|
+
return text(lines.join("\n"));
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
server2.tool(
|
|
1160
|
+
"tunnel_destroy",
|
|
1161
|
+
"Immediately destroy a tunneled secret.",
|
|
1162
|
+
{
|
|
1163
|
+
id: z.string().describe("Tunnel ID")
|
|
1164
|
+
},
|
|
1165
|
+
async (params) => {
|
|
1166
|
+
const destroyed = tunnelDestroy(params.id);
|
|
1167
|
+
return text(
|
|
1168
|
+
destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
|
|
1169
|
+
!destroyed
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
);
|
|
1173
|
+
server2.tool(
|
|
1174
|
+
"teleport_pack",
|
|
1175
|
+
"Pack secrets into an AES-256-GCM encrypted bundle for sharing between machines (quantum teleportation).",
|
|
1176
|
+
{
|
|
1177
|
+
keys: z.array(z.string()).optional().describe("Specific keys to pack (all if omitted)"),
|
|
1178
|
+
passphrase: z.string().describe("Encryption passphrase"),
|
|
1179
|
+
scope: scopeSchema,
|
|
1180
|
+
projectPath: projectPathSchema
|
|
1181
|
+
},
|
|
1182
|
+
async (params) => {
|
|
1183
|
+
const o = opts(params);
|
|
1184
|
+
const entries = listSecrets(o);
|
|
1185
|
+
const secrets = [];
|
|
1186
|
+
for (const entry of entries) {
|
|
1187
|
+
if (params.keys && !params.keys.includes(entry.key)) continue;
|
|
1188
|
+
const value = getSecret(entry.key, { ...o, scope: entry.scope });
|
|
1189
|
+
if (value !== null) {
|
|
1190
|
+
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (secrets.length === 0) return text("No secrets to pack", true);
|
|
1194
|
+
const bundle = teleportPack(secrets, params.passphrase);
|
|
1195
|
+
return text(bundle);
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
server2.tool(
|
|
1199
|
+
"teleport_unpack",
|
|
1200
|
+
"Decrypt and import secrets from a teleport bundle.",
|
|
1201
|
+
{
|
|
1202
|
+
bundle: z.string().describe("Base64-encoded encrypted bundle"),
|
|
1203
|
+
passphrase: z.string().describe("Decryption passphrase"),
|
|
1204
|
+
scope: scopeSchema.default("global"),
|
|
1205
|
+
projectPath: projectPathSchema,
|
|
1206
|
+
dryRun: z.boolean().optional().default(false).describe("Preview without importing")
|
|
1207
|
+
},
|
|
1208
|
+
async (params) => {
|
|
1209
|
+
try {
|
|
1210
|
+
const payload = teleportUnpack(params.bundle, params.passphrase);
|
|
1211
|
+
if (params.dryRun) {
|
|
1212
|
+
const preview = payload.secrets.map((s) => `${s.key} [${s.scope ?? "global"}]`).join("\n");
|
|
1213
|
+
return text(`Would import ${payload.secrets.length} secrets:
|
|
1214
|
+
${preview}`);
|
|
1215
|
+
}
|
|
1216
|
+
const o = opts(params);
|
|
1217
|
+
for (const s of payload.secrets) {
|
|
1218
|
+
setSecret(s.key, s.value, o);
|
|
1219
|
+
}
|
|
1220
|
+
return text(`Imported ${payload.secrets.length} secret(s) from teleport bundle`);
|
|
1221
|
+
} catch {
|
|
1222
|
+
return text("Failed to unpack: wrong passphrase or corrupted bundle", true);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
server2.tool(
|
|
1227
|
+
"audit_log",
|
|
1228
|
+
"Query the audit log for secret access history (observer effect). Shows who accessed what and when.",
|
|
1229
|
+
{
|
|
1230
|
+
key: z.string().optional().describe("Filter by key"),
|
|
1231
|
+
action: z.enum(["read", "write", "delete", "list", "export", "generate", "entangle", "tunnel", "teleport", "collapse"]).optional().describe("Filter by action"),
|
|
1232
|
+
limit: z.number().optional().default(20).describe("Max events to return")
|
|
1233
|
+
},
|
|
1234
|
+
async (params) => {
|
|
1235
|
+
const events = queryAudit({
|
|
1236
|
+
key: params.key,
|
|
1237
|
+
action: params.action,
|
|
1238
|
+
limit: params.limit
|
|
1239
|
+
});
|
|
1240
|
+
if (events.length === 0) return text("No audit events found");
|
|
1241
|
+
const lines = events.map((e) => {
|
|
1242
|
+
const parts = [e.timestamp, e.action];
|
|
1243
|
+
if (e.key) parts.push(e.key);
|
|
1244
|
+
if (e.scope) parts.push(`[${e.scope}]`);
|
|
1245
|
+
if (e.env) parts.push(`env:${e.env}`);
|
|
1246
|
+
if (e.detail) parts.push(e.detail);
|
|
1247
|
+
return parts.join(" | ");
|
|
1248
|
+
});
|
|
1249
|
+
return text(lines.join("\n"));
|
|
1250
|
+
}
|
|
1251
|
+
);
|
|
1252
|
+
server2.tool(
|
|
1253
|
+
"detect_anomalies",
|
|
1254
|
+
"Scan for anomalous secret access patterns: burst reads, unusual-hour access. Returns findings and recommendations.",
|
|
1255
|
+
{
|
|
1256
|
+
key: z.string().optional().describe("Check anomalies for a specific key")
|
|
1257
|
+
},
|
|
1258
|
+
async (params) => {
|
|
1259
|
+
const anomalies = detectAnomalies(params.key);
|
|
1260
|
+
if (anomalies.length === 0) return text("No anomalies detected");
|
|
1261
|
+
const lines = anomalies.map(
|
|
1262
|
+
(a) => `[${a.type}] ${a.description}`
|
|
1263
|
+
);
|
|
1264
|
+
return text(lines.join("\n"));
|
|
1265
|
+
}
|
|
1266
|
+
);
|
|
1267
|
+
server2.tool(
|
|
1268
|
+
"health_check",
|
|
1269
|
+
"Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
|
|
1270
|
+
{
|
|
1271
|
+
scope: scopeSchema,
|
|
1272
|
+
projectPath: projectPathSchema
|
|
1273
|
+
},
|
|
1274
|
+
async (params) => {
|
|
1275
|
+
const entries = listSecrets(opts(params));
|
|
1276
|
+
const anomalies = detectAnomalies();
|
|
1277
|
+
let healthy = 0;
|
|
1278
|
+
let stale = 0;
|
|
1279
|
+
let expired = 0;
|
|
1280
|
+
let noDecay = 0;
|
|
1281
|
+
const issues = [];
|
|
1282
|
+
for (const entry of entries) {
|
|
1283
|
+
if (!entry.decay?.timeRemaining) {
|
|
1284
|
+
noDecay++;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
if (entry.decay.isExpired) {
|
|
1288
|
+
expired++;
|
|
1289
|
+
issues.push(`EXPIRED: ${entry.key}`);
|
|
1290
|
+
} else if (entry.decay.isStale) {
|
|
1291
|
+
stale++;
|
|
1292
|
+
issues.push(
|
|
1293
|
+
`STALE: ${entry.key} (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`
|
|
1294
|
+
);
|
|
1295
|
+
} else {
|
|
1296
|
+
healthy++;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const summary = [
|
|
1300
|
+
`Secrets: ${entries.length} total`,
|
|
1301
|
+
`Healthy: ${healthy} | Stale: ${stale} | Expired: ${expired} | No decay: ${noDecay}`,
|
|
1302
|
+
`Anomalies: ${anomalies.length}`
|
|
1303
|
+
];
|
|
1304
|
+
if (issues.length > 0) {
|
|
1305
|
+
summary.push("", "Issues:", ...issues);
|
|
1306
|
+
}
|
|
1307
|
+
if (anomalies.length > 0) {
|
|
1308
|
+
summary.push(
|
|
1309
|
+
"",
|
|
1310
|
+
"Anomalies:",
|
|
1311
|
+
...anomalies.map((a) => `[${a.type}] ${a.description}`)
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
return text(summary.join("\n"));
|
|
1315
|
+
}
|
|
1316
|
+
);
|
|
1317
|
+
server2.tool(
|
|
1318
|
+
"agent_scan",
|
|
1319
|
+
"Run an autonomous agent health scan: checks decay, staleness, anomalies, and optionally auto-rotates expired secrets. Returns a structured report.",
|
|
1320
|
+
{
|
|
1321
|
+
autoRotate: z.boolean().optional().default(false).describe("Auto-rotate expired secrets with generated values"),
|
|
1322
|
+
projectPaths: z.array(z.string()).optional().describe("Project paths to monitor")
|
|
1323
|
+
},
|
|
1324
|
+
async (params) => {
|
|
1325
|
+
const report = runHealthScan({
|
|
1326
|
+
autoRotate: params.autoRotate,
|
|
1327
|
+
projectPaths: params.projectPaths ?? [process.cwd()]
|
|
1328
|
+
});
|
|
1329
|
+
return text(JSON.stringify(report, null, 2));
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
return server2;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/mcp.ts
|
|
1336
|
+
var server = createMcpServer();
|
|
1337
|
+
var transport = new StdioServerTransport();
|
|
1338
|
+
await server.connect(transport);
|
|
1339
|
+
//# sourceMappingURL=mcp.js.map
|