@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/index.js
ADDED
|
@@ -0,0 +1,1525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/commands.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/core/keyring.ts
|
|
7
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
8
|
+
|
|
9
|
+
// src/utils/hash.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
function hashProjectPath(projectPath) {
|
|
12
|
+
return createHash("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/core/scope.ts
|
|
16
|
+
var SERVICE_PREFIX = "q-ring";
|
|
17
|
+
function globalService() {
|
|
18
|
+
return `${SERVICE_PREFIX}:global`;
|
|
19
|
+
}
|
|
20
|
+
function projectService(projectPath) {
|
|
21
|
+
const hash = hashProjectPath(projectPath);
|
|
22
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
23
|
+
}
|
|
24
|
+
function resolveScope(opts) {
|
|
25
|
+
const { scope, projectPath } = opts;
|
|
26
|
+
if (scope === "global") {
|
|
27
|
+
return [{ scope: "global", service: globalService() }];
|
|
28
|
+
}
|
|
29
|
+
if (scope === "project") {
|
|
30
|
+
if (!projectPath) {
|
|
31
|
+
throw new Error("Project path is required for project scope");
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
{ scope: "project", service: projectService(projectPath), projectPath }
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
if (projectPath) {
|
|
38
|
+
return [
|
|
39
|
+
{ scope: "project", service: projectService(projectPath), projectPath },
|
|
40
|
+
{ scope: "global", service: globalService() }
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
return [{ scope: "global", service: globalService() }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/envelope.ts
|
|
47
|
+
function createEnvelope(value, opts) {
|
|
48
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
49
|
+
let expiresAt = opts?.expiresAt;
|
|
50
|
+
if (!expiresAt && opts?.ttlSeconds) {
|
|
51
|
+
expiresAt = new Date(Date.now() + opts.ttlSeconds * 1e3).toISOString();
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
v: 1,
|
|
55
|
+
value: opts?.states ? void 0 : value,
|
|
56
|
+
states: opts?.states,
|
|
57
|
+
defaultEnv: opts?.defaultEnv,
|
|
58
|
+
meta: {
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
expiresAt,
|
|
62
|
+
ttlSeconds: opts?.ttlSeconds,
|
|
63
|
+
description: opts?.description,
|
|
64
|
+
tags: opts?.tags,
|
|
65
|
+
entangled: opts?.entangled,
|
|
66
|
+
accessCount: 0
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseEnvelope(raw) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (parsed && typeof parsed === "object" && parsed.v === 1) {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function wrapLegacy(rawValue) {
|
|
81
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
82
|
+
return {
|
|
83
|
+
v: 1,
|
|
84
|
+
value: rawValue,
|
|
85
|
+
meta: {
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
accessCount: 0
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function serializeEnvelope(envelope) {
|
|
93
|
+
return JSON.stringify(envelope);
|
|
94
|
+
}
|
|
95
|
+
function collapseValue(envelope, env) {
|
|
96
|
+
if (envelope.states) {
|
|
97
|
+
const targetEnv = env ?? envelope.defaultEnv;
|
|
98
|
+
if (targetEnv && envelope.states[targetEnv]) {
|
|
99
|
+
return envelope.states[targetEnv];
|
|
100
|
+
}
|
|
101
|
+
if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
|
|
102
|
+
return envelope.states[envelope.defaultEnv];
|
|
103
|
+
}
|
|
104
|
+
const keys = Object.keys(envelope.states);
|
|
105
|
+
if (keys.length > 0) {
|
|
106
|
+
return envelope.states[keys[0]];
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return envelope.value ?? null;
|
|
111
|
+
}
|
|
112
|
+
function checkDecay(envelope) {
|
|
113
|
+
if (!envelope.meta.expiresAt) {
|
|
114
|
+
return {
|
|
115
|
+
isExpired: false,
|
|
116
|
+
isStale: false,
|
|
117
|
+
lifetimePercent: 0,
|
|
118
|
+
secondsRemaining: null,
|
|
119
|
+
timeRemaining: null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
124
|
+
const created = new Date(envelope.meta.createdAt).getTime();
|
|
125
|
+
const totalLifetime = expires - created;
|
|
126
|
+
const elapsed = now - created;
|
|
127
|
+
const remaining = expires - now;
|
|
128
|
+
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
129
|
+
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
130
|
+
let timeRemaining = null;
|
|
131
|
+
if (remaining > 0) {
|
|
132
|
+
const days = Math.floor(remaining / 864e5);
|
|
133
|
+
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
134
|
+
const minutes = Math.floor(remaining % 36e5 / 6e4);
|
|
135
|
+
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
136
|
+
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
137
|
+
else timeRemaining = `${minutes}m`;
|
|
138
|
+
} else {
|
|
139
|
+
timeRemaining = "expired";
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
isExpired: remaining <= 0,
|
|
143
|
+
isStale: lifetimePercent >= 75,
|
|
144
|
+
lifetimePercent,
|
|
145
|
+
secondsRemaining,
|
|
146
|
+
timeRemaining
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function recordAccess(envelope) {
|
|
150
|
+
return {
|
|
151
|
+
...envelope,
|
|
152
|
+
meta: {
|
|
153
|
+
...envelope.meta,
|
|
154
|
+
accessCount: envelope.meta.accessCount + 1,
|
|
155
|
+
lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/core/collapse.ts
|
|
161
|
+
import { execSync } from "child_process";
|
|
162
|
+
import { existsSync, readFileSync } from "fs";
|
|
163
|
+
import { join } from "path";
|
|
164
|
+
var BRANCH_ENV_MAP = {
|
|
165
|
+
main: "prod",
|
|
166
|
+
master: "prod",
|
|
167
|
+
production: "prod",
|
|
168
|
+
develop: "dev",
|
|
169
|
+
development: "dev",
|
|
170
|
+
dev: "dev",
|
|
171
|
+
staging: "staging",
|
|
172
|
+
stage: "staging",
|
|
173
|
+
test: "test",
|
|
174
|
+
testing: "test"
|
|
175
|
+
};
|
|
176
|
+
function detectGitBranch(cwd) {
|
|
177
|
+
try {
|
|
178
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
179
|
+
cwd: cwd ?? process.cwd(),
|
|
180
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
timeout: 3e3
|
|
183
|
+
}).trim();
|
|
184
|
+
return branch || null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function readProjectConfig(projectPath) {
|
|
190
|
+
const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
|
|
191
|
+
try {
|
|
192
|
+
if (existsSync(configPath)) {
|
|
193
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
function collapseEnvironment(ctx = {}) {
|
|
200
|
+
if (ctx.explicit) {
|
|
201
|
+
return { env: ctx.explicit, source: "explicit" };
|
|
202
|
+
}
|
|
203
|
+
const qringEnv = process.env.QRING_ENV;
|
|
204
|
+
if (qringEnv) {
|
|
205
|
+
return { env: qringEnv, source: "QRING_ENV" };
|
|
206
|
+
}
|
|
207
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
208
|
+
if (nodeEnv) {
|
|
209
|
+
const mapped = mapEnvName(nodeEnv);
|
|
210
|
+
return { env: mapped, source: "NODE_ENV" };
|
|
211
|
+
}
|
|
212
|
+
const config = readProjectConfig(ctx.projectPath);
|
|
213
|
+
if (config?.env) {
|
|
214
|
+
return { env: config.env, source: "project-config" };
|
|
215
|
+
}
|
|
216
|
+
const branch = detectGitBranch(ctx.projectPath);
|
|
217
|
+
if (branch) {
|
|
218
|
+
const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
|
|
219
|
+
const mapped = branchMap[branch];
|
|
220
|
+
if (mapped) {
|
|
221
|
+
return { env: mapped, source: "git-branch" };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (config?.defaultEnv) {
|
|
225
|
+
return { env: config.defaultEnv, source: "project-config" };
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
function mapEnvName(raw) {
|
|
230
|
+
const lower = raw.toLowerCase();
|
|
231
|
+
if (lower === "production") return "prod";
|
|
232
|
+
if (lower === "development") return "dev";
|
|
233
|
+
return lower;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/core/observer.ts
|
|
237
|
+
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2 } from "fs";
|
|
238
|
+
import { join as join2 } from "path";
|
|
239
|
+
import { homedir } from "os";
|
|
240
|
+
function getAuditDir() {
|
|
241
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
242
|
+
if (!existsSync2(dir)) {
|
|
243
|
+
mkdirSync(dir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
return dir;
|
|
246
|
+
}
|
|
247
|
+
function getAuditPath() {
|
|
248
|
+
return join2(getAuditDir(), "audit.jsonl");
|
|
249
|
+
}
|
|
250
|
+
function logAudit(event) {
|
|
251
|
+
const full = {
|
|
252
|
+
...event,
|
|
253
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
254
|
+
pid: process.pid
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function queryAudit(query = {}) {
|
|
262
|
+
const path = getAuditPath();
|
|
263
|
+
if (!existsSync2(path)) return [];
|
|
264
|
+
try {
|
|
265
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
266
|
+
let events = lines.map((line) => {
|
|
267
|
+
try {
|
|
268
|
+
return JSON.parse(line);
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}).filter((e) => e !== null);
|
|
273
|
+
if (query.key) {
|
|
274
|
+
events = events.filter((e) => e.key === query.key);
|
|
275
|
+
}
|
|
276
|
+
if (query.action) {
|
|
277
|
+
events = events.filter((e) => e.action === query.action);
|
|
278
|
+
}
|
|
279
|
+
if (query.since) {
|
|
280
|
+
const since = new Date(query.since).getTime();
|
|
281
|
+
events = events.filter(
|
|
282
|
+
(e) => new Date(e.timestamp).getTime() >= since
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
events.sort(
|
|
286
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
287
|
+
);
|
|
288
|
+
if (query.limit) {
|
|
289
|
+
events = events.slice(0, query.limit);
|
|
290
|
+
}
|
|
291
|
+
return events;
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function detectAnomalies(key) {
|
|
297
|
+
const recent = queryAudit({
|
|
298
|
+
key,
|
|
299
|
+
action: "read",
|
|
300
|
+
since: new Date(Date.now() - 36e5).toISOString()
|
|
301
|
+
// last hour
|
|
302
|
+
});
|
|
303
|
+
const anomalies = [];
|
|
304
|
+
if (key && recent.length > 50) {
|
|
305
|
+
anomalies.push({
|
|
306
|
+
type: "burst",
|
|
307
|
+
description: `${recent.length} reads of "${key}" in the last hour`,
|
|
308
|
+
events: recent.slice(0, 10)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const nightAccess = recent.filter((e) => {
|
|
312
|
+
const hour = new Date(e.timestamp).getHours();
|
|
313
|
+
return hour >= 1 && hour < 5;
|
|
314
|
+
});
|
|
315
|
+
if (nightAccess.length > 0) {
|
|
316
|
+
anomalies.push({
|
|
317
|
+
type: "unusual-hour",
|
|
318
|
+
description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
|
|
319
|
+
events: nightAccess
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return anomalies;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/core/entanglement.ts
|
|
326
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
327
|
+
import { join as join3 } from "path";
|
|
328
|
+
import { homedir as homedir2 } from "os";
|
|
329
|
+
function getRegistryPath() {
|
|
330
|
+
const dir = join3(homedir2(), ".config", "q-ring");
|
|
331
|
+
if (!existsSync3(dir)) {
|
|
332
|
+
mkdirSync2(dir, { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
return join3(dir, "entanglement.json");
|
|
335
|
+
}
|
|
336
|
+
function loadRegistry() {
|
|
337
|
+
const path = getRegistryPath();
|
|
338
|
+
if (!existsSync3(path)) {
|
|
339
|
+
return { pairs: [] };
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
343
|
+
} catch {
|
|
344
|
+
return { pairs: [] };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function saveRegistry(registry) {
|
|
348
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));
|
|
349
|
+
}
|
|
350
|
+
function entangle(source, target) {
|
|
351
|
+
const registry = loadRegistry();
|
|
352
|
+
const exists = registry.pairs.some(
|
|
353
|
+
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
354
|
+
);
|
|
355
|
+
if (!exists) {
|
|
356
|
+
registry.pairs.push({
|
|
357
|
+
source,
|
|
358
|
+
target,
|
|
359
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
360
|
+
});
|
|
361
|
+
registry.pairs.push({
|
|
362
|
+
source: target,
|
|
363
|
+
target: source,
|
|
364
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
365
|
+
});
|
|
366
|
+
saveRegistry(registry);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function findEntangled(source) {
|
|
370
|
+
const registry = loadRegistry();
|
|
371
|
+
return registry.pairs.filter(
|
|
372
|
+
(p) => p.source.service === source.service && p.source.key === source.key
|
|
373
|
+
).map((p) => p.target);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/keyring.ts
|
|
377
|
+
function readEnvelope(service, key) {
|
|
378
|
+
const entry = new Entry(service, key);
|
|
379
|
+
const raw = entry.getPassword();
|
|
380
|
+
if (raw === null) return null;
|
|
381
|
+
const envelope = parseEnvelope(raw);
|
|
382
|
+
return envelope ?? wrapLegacy(raw);
|
|
383
|
+
}
|
|
384
|
+
function writeEnvelope(service, key, envelope) {
|
|
385
|
+
const entry = new Entry(service, key);
|
|
386
|
+
entry.setPassword(serializeEnvelope(envelope));
|
|
387
|
+
}
|
|
388
|
+
function resolveEnv(opts) {
|
|
389
|
+
if (opts.env) return opts.env;
|
|
390
|
+
const result = collapseEnvironment({ projectPath: opts.projectPath });
|
|
391
|
+
return result?.env;
|
|
392
|
+
}
|
|
393
|
+
function getSecret(key, opts = {}) {
|
|
394
|
+
const scopes = resolveScope(opts);
|
|
395
|
+
const env = resolveEnv(opts);
|
|
396
|
+
const source = opts.source ?? "cli";
|
|
397
|
+
for (const { service, scope } of scopes) {
|
|
398
|
+
const envelope = readEnvelope(service, key);
|
|
399
|
+
if (!envelope) continue;
|
|
400
|
+
const decay = checkDecay(envelope);
|
|
401
|
+
if (decay.isExpired) {
|
|
402
|
+
logAudit({
|
|
403
|
+
action: "read",
|
|
404
|
+
key,
|
|
405
|
+
scope,
|
|
406
|
+
source,
|
|
407
|
+
detail: "blocked: secret expired (quantum decay)"
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const value = collapseValue(envelope, env);
|
|
412
|
+
if (value === null) continue;
|
|
413
|
+
const updated = recordAccess(envelope);
|
|
414
|
+
writeEnvelope(service, key, updated);
|
|
415
|
+
logAudit({ action: "read", key, scope, env, source });
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function getEnvelope(key, opts = {}) {
|
|
421
|
+
const scopes = resolveScope(opts);
|
|
422
|
+
for (const { service, scope } of scopes) {
|
|
423
|
+
const envelope = readEnvelope(service, key);
|
|
424
|
+
if (envelope) return { envelope, scope };
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function setSecret(key, value, opts = {}) {
|
|
429
|
+
const scope = opts.scope ?? "global";
|
|
430
|
+
const scopes = resolveScope({ ...opts, scope });
|
|
431
|
+
const { service } = scopes[0];
|
|
432
|
+
const source = opts.source ?? "cli";
|
|
433
|
+
const existing = readEnvelope(service, key);
|
|
434
|
+
let envelope;
|
|
435
|
+
if (opts.states) {
|
|
436
|
+
envelope = createEnvelope("", {
|
|
437
|
+
states: opts.states,
|
|
438
|
+
defaultEnv: opts.defaultEnv,
|
|
439
|
+
description: opts.description,
|
|
440
|
+
tags: opts.tags,
|
|
441
|
+
ttlSeconds: opts.ttlSeconds,
|
|
442
|
+
expiresAt: opts.expiresAt,
|
|
443
|
+
entangled: existing?.meta.entangled
|
|
444
|
+
});
|
|
445
|
+
} else {
|
|
446
|
+
envelope = createEnvelope(value, {
|
|
447
|
+
description: opts.description,
|
|
448
|
+
tags: opts.tags,
|
|
449
|
+
ttlSeconds: opts.ttlSeconds,
|
|
450
|
+
expiresAt: opts.expiresAt,
|
|
451
|
+
entangled: existing?.meta.entangled
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (existing) {
|
|
455
|
+
envelope.meta.createdAt = existing.meta.createdAt;
|
|
456
|
+
envelope.meta.accessCount = existing.meta.accessCount;
|
|
457
|
+
}
|
|
458
|
+
writeEnvelope(service, key, envelope);
|
|
459
|
+
logAudit({ action: "write", key, scope, source });
|
|
460
|
+
const entangled = findEntangled({ service, key });
|
|
461
|
+
for (const target of entangled) {
|
|
462
|
+
try {
|
|
463
|
+
const targetEnvelope = readEnvelope(target.service, target.key);
|
|
464
|
+
if (targetEnvelope) {
|
|
465
|
+
if (opts.states) {
|
|
466
|
+
targetEnvelope.states = opts.states;
|
|
467
|
+
} else {
|
|
468
|
+
targetEnvelope.value = value;
|
|
469
|
+
}
|
|
470
|
+
targetEnvelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
471
|
+
writeEnvelope(target.service, target.key, targetEnvelope);
|
|
472
|
+
logAudit({
|
|
473
|
+
action: "entangle",
|
|
474
|
+
key: target.key,
|
|
475
|
+
scope: "global",
|
|
476
|
+
source,
|
|
477
|
+
detail: `propagated from ${key}`
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function deleteSecret(key, opts = {}) {
|
|
485
|
+
const scopes = resolveScope(opts);
|
|
486
|
+
const source = opts.source ?? "cli";
|
|
487
|
+
let deleted = false;
|
|
488
|
+
for (const { service, scope } of scopes) {
|
|
489
|
+
const entry = new Entry(service, key);
|
|
490
|
+
try {
|
|
491
|
+
if (entry.deleteCredential()) {
|
|
492
|
+
deleted = true;
|
|
493
|
+
logAudit({ action: "delete", key, scope, source });
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return deleted;
|
|
499
|
+
}
|
|
500
|
+
function listSecrets(opts = {}) {
|
|
501
|
+
const source = opts.source ?? "cli";
|
|
502
|
+
const services = [];
|
|
503
|
+
if (!opts.scope || opts.scope === "global") {
|
|
504
|
+
services.push({ service: globalService(), scope: "global" });
|
|
505
|
+
}
|
|
506
|
+
if ((!opts.scope || opts.scope === "project") && opts.projectPath) {
|
|
507
|
+
services.push({
|
|
508
|
+
service: projectService(opts.projectPath),
|
|
509
|
+
scope: "project"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const results = [];
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
for (const { service, scope } of services) {
|
|
515
|
+
try {
|
|
516
|
+
const credentials = findCredentials(service);
|
|
517
|
+
for (const cred of credentials) {
|
|
518
|
+
const id = `${scope}:${cred.account}`;
|
|
519
|
+
if (seen.has(id)) continue;
|
|
520
|
+
seen.add(id);
|
|
521
|
+
const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
|
|
522
|
+
const decay = checkDecay(envelope);
|
|
523
|
+
results.push({
|
|
524
|
+
key: cred.account,
|
|
525
|
+
scope,
|
|
526
|
+
envelope,
|
|
527
|
+
decay
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
logAudit({ action: "list", source });
|
|
534
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
535
|
+
}
|
|
536
|
+
function exportSecrets(opts = {}) {
|
|
537
|
+
const format = opts.format ?? "env";
|
|
538
|
+
const env = resolveEnv(opts);
|
|
539
|
+
const entries = listSecrets(opts);
|
|
540
|
+
const source = opts.source ?? "cli";
|
|
541
|
+
const merged = /* @__PURE__ */ new Map();
|
|
542
|
+
const globalEntries = entries.filter((e) => e.scope === "global");
|
|
543
|
+
const projectEntries = entries.filter((e) => e.scope === "project");
|
|
544
|
+
for (const entry of [...globalEntries, ...projectEntries]) {
|
|
545
|
+
if (entry.envelope) {
|
|
546
|
+
const decay = checkDecay(entry.envelope);
|
|
547
|
+
if (decay.isExpired) continue;
|
|
548
|
+
const value = collapseValue(entry.envelope, env);
|
|
549
|
+
if (value !== null) {
|
|
550
|
+
merged.set(entry.key, value);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
logAudit({ action: "export", source, detail: `format=${format}` });
|
|
555
|
+
if (format === "json") {
|
|
556
|
+
const obj = {};
|
|
557
|
+
for (const [key, value] of merged) {
|
|
558
|
+
obj[key] = value;
|
|
559
|
+
}
|
|
560
|
+
return JSON.stringify(obj, null, 2);
|
|
561
|
+
}
|
|
562
|
+
const lines = [];
|
|
563
|
+
for (const [key, value] of merged) {
|
|
564
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
565
|
+
lines.push(`${key}="${escaped}"`);
|
|
566
|
+
}
|
|
567
|
+
return lines.join("\n");
|
|
568
|
+
}
|
|
569
|
+
function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
570
|
+
const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
|
|
571
|
+
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
572
|
+
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
573
|
+
const target = { service: targetScopes[0].service, key: targetKey };
|
|
574
|
+
entangle(source, target);
|
|
575
|
+
logAudit({
|
|
576
|
+
action: "entangle",
|
|
577
|
+
key: sourceKey,
|
|
578
|
+
source: sourceOpts.source ?? "cli",
|
|
579
|
+
detail: `entangled with ${targetKey}`
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/core/noise.ts
|
|
584
|
+
import { randomBytes, randomInt } from "crypto";
|
|
585
|
+
var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
586
|
+
var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
587
|
+
function randomString(charset, length) {
|
|
588
|
+
let result = "";
|
|
589
|
+
for (let i = 0; i < length; i++) {
|
|
590
|
+
result += charset[randomInt(charset.length)];
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
function generateSecret(opts = {}) {
|
|
595
|
+
const format = opts.format ?? "api-key";
|
|
596
|
+
switch (format) {
|
|
597
|
+
case "hex": {
|
|
598
|
+
const len = opts.length ?? 32;
|
|
599
|
+
return randomBytes(len).toString("hex");
|
|
600
|
+
}
|
|
601
|
+
case "base64": {
|
|
602
|
+
const len = opts.length ?? 32;
|
|
603
|
+
return randomBytes(len).toString("base64url");
|
|
604
|
+
}
|
|
605
|
+
case "alphanumeric": {
|
|
606
|
+
const len = opts.length ?? 32;
|
|
607
|
+
return randomString(ALPHA_NUM, len);
|
|
608
|
+
}
|
|
609
|
+
case "uuid": {
|
|
610
|
+
const bytes = randomBytes(16);
|
|
611
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
612
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
613
|
+
const hex = bytes.toString("hex");
|
|
614
|
+
return [
|
|
615
|
+
hex.slice(0, 8),
|
|
616
|
+
hex.slice(8, 12),
|
|
617
|
+
hex.slice(12, 16),
|
|
618
|
+
hex.slice(16, 20),
|
|
619
|
+
hex.slice(20, 32)
|
|
620
|
+
].join("-");
|
|
621
|
+
}
|
|
622
|
+
case "api-key": {
|
|
623
|
+
const prefix = opts.prefix ?? "qr_";
|
|
624
|
+
const len = opts.length ?? 48;
|
|
625
|
+
return prefix + randomString(ALPHA_NUM, len);
|
|
626
|
+
}
|
|
627
|
+
case "token": {
|
|
628
|
+
const prefix = opts.prefix ?? "";
|
|
629
|
+
const len = opts.length ?? 64;
|
|
630
|
+
return prefix + randomBytes(len).toString("base64url");
|
|
631
|
+
}
|
|
632
|
+
case "password": {
|
|
633
|
+
const len = opts.length ?? 24;
|
|
634
|
+
let pw = randomString(PASSWORD_CHARS, len);
|
|
635
|
+
const hasUpper = /[A-Z]/.test(pw);
|
|
636
|
+
const hasLower = /[a-z]/.test(pw);
|
|
637
|
+
const hasDigit = /[0-9]/.test(pw);
|
|
638
|
+
const hasSpecial = /[^A-Za-z0-9]/.test(pw);
|
|
639
|
+
if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
|
|
640
|
+
if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
|
|
641
|
+
if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
|
|
642
|
+
if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
|
|
643
|
+
return pw;
|
|
644
|
+
}
|
|
645
|
+
default:
|
|
646
|
+
return randomBytes(32).toString("hex");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function replaceAt(str, index, char) {
|
|
650
|
+
return str.slice(0, index) + char + str.slice(index + 1);
|
|
651
|
+
}
|
|
652
|
+
function estimateEntropy(secret) {
|
|
653
|
+
const charsets = [
|
|
654
|
+
{ regex: /[a-z]/, size: 26 },
|
|
655
|
+
{ regex: /[A-Z]/, size: 26 },
|
|
656
|
+
{ regex: /[0-9]/, size: 10 },
|
|
657
|
+
{ regex: /[^A-Za-z0-9]/, size: 32 }
|
|
658
|
+
];
|
|
659
|
+
let poolSize = 0;
|
|
660
|
+
for (const { regex, size } of charsets) {
|
|
661
|
+
if (regex.test(secret)) poolSize += size;
|
|
662
|
+
}
|
|
663
|
+
return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/utils/colors.ts
|
|
667
|
+
var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
668
|
+
function wrap(code, text) {
|
|
669
|
+
return enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
670
|
+
}
|
|
671
|
+
var c = {
|
|
672
|
+
bold: (t) => wrap("1", t),
|
|
673
|
+
dim: (t) => wrap("2", t),
|
|
674
|
+
italic: (t) => wrap("3", t),
|
|
675
|
+
underline: (t) => wrap("4", t),
|
|
676
|
+
red: (t) => wrap("31", t),
|
|
677
|
+
green: (t) => wrap("32", t),
|
|
678
|
+
yellow: (t) => wrap("33", t),
|
|
679
|
+
blue: (t) => wrap("34", t),
|
|
680
|
+
magenta: (t) => wrap("35", t),
|
|
681
|
+
cyan: (t) => wrap("36", t),
|
|
682
|
+
white: (t) => wrap("37", t),
|
|
683
|
+
gray: (t) => wrap("90", t),
|
|
684
|
+
bgRed: (t) => wrap("41", t),
|
|
685
|
+
bgGreen: (t) => wrap("42", t),
|
|
686
|
+
bgYellow: (t) => wrap("43", t),
|
|
687
|
+
bgBlue: (t) => wrap("44", t),
|
|
688
|
+
bgMagenta: (t) => wrap("45", t),
|
|
689
|
+
bgCyan: (t) => wrap("46", t)
|
|
690
|
+
};
|
|
691
|
+
function scopeColor(scope) {
|
|
692
|
+
return scope === "project" ? c.cyan(scope) : c.blue(scope);
|
|
693
|
+
}
|
|
694
|
+
function decayIndicator(percent, expired) {
|
|
695
|
+
if (expired) return c.bgRed(c.white(" EXPIRED "));
|
|
696
|
+
if (percent >= 90) return c.red(`[decay ${percent}%]`);
|
|
697
|
+
if (percent >= 75) return c.yellow(`[decay ${percent}%]`);
|
|
698
|
+
if (percent > 0) return c.green(`[decay ${percent}%]`);
|
|
699
|
+
return "";
|
|
700
|
+
}
|
|
701
|
+
function envBadge(env) {
|
|
702
|
+
switch (env) {
|
|
703
|
+
case "prod":
|
|
704
|
+
return c.bgRed(c.white(` ${env} `));
|
|
705
|
+
case "staging":
|
|
706
|
+
return c.bgYellow(c.white(` ${env} `));
|
|
707
|
+
case "dev":
|
|
708
|
+
return c.bgGreen(c.white(` ${env} `));
|
|
709
|
+
case "test":
|
|
710
|
+
return c.bgBlue(c.white(` ${env} `));
|
|
711
|
+
default:
|
|
712
|
+
return c.bgMagenta(c.white(` ${env} `));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
var SYMBOLS = {
|
|
716
|
+
check: enabled ? "\u2713" : "[ok]",
|
|
717
|
+
cross: enabled ? "\u2717" : "[x]",
|
|
718
|
+
arrow: enabled ? "\u2192" : "->",
|
|
719
|
+
dot: enabled ? "\u2022" : "*",
|
|
720
|
+
lock: enabled ? "\u{1F512}" : "[locked]",
|
|
721
|
+
key: enabled ? "\u{1F511}" : "[key]",
|
|
722
|
+
link: enabled ? "\u{1F517}" : "[link]",
|
|
723
|
+
warning: enabled ? "\u26A0\uFE0F" : "[!]",
|
|
724
|
+
clock: enabled ? "\u23F0" : "[time]",
|
|
725
|
+
shield: enabled ? "\u{1F6E1}\uFE0F" : "[shield]",
|
|
726
|
+
zap: enabled ? "\u26A1" : "[zap]",
|
|
727
|
+
eye: enabled ? "\u{1F441}\uFE0F" : "[eye]",
|
|
728
|
+
ghost: enabled ? "\u{1F47B}" : "[ghost]",
|
|
729
|
+
package: enabled ? "\u{1F4E6}" : "[pkg]",
|
|
730
|
+
sparkle: enabled ? "\u2728" : "[*]"
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/core/agent.ts
|
|
734
|
+
function defaultConfig() {
|
|
735
|
+
return {
|
|
736
|
+
intervalSeconds: 60,
|
|
737
|
+
autoRotate: false,
|
|
738
|
+
projectPaths: [process.cwd()],
|
|
739
|
+
verbose: false
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function runHealthScan(config = {}) {
|
|
743
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
744
|
+
const report = {
|
|
745
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
746
|
+
totalSecrets: 0,
|
|
747
|
+
healthy: 0,
|
|
748
|
+
stale: 0,
|
|
749
|
+
expired: 0,
|
|
750
|
+
anomalies: 0,
|
|
751
|
+
rotated: [],
|
|
752
|
+
warnings: []
|
|
753
|
+
};
|
|
754
|
+
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
755
|
+
const projectEntries = cfg.projectPaths.flatMap(
|
|
756
|
+
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
757
|
+
);
|
|
758
|
+
const allEntries = [...globalEntries, ...projectEntries];
|
|
759
|
+
report.totalSecrets = allEntries.length;
|
|
760
|
+
for (const entry of allEntries) {
|
|
761
|
+
if (!entry.envelope) continue;
|
|
762
|
+
const decay = checkDecay(entry.envelope);
|
|
763
|
+
if (decay.isExpired) {
|
|
764
|
+
report.expired++;
|
|
765
|
+
report.warnings.push(
|
|
766
|
+
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
767
|
+
);
|
|
768
|
+
if (cfg.autoRotate) {
|
|
769
|
+
const newValue = generateSecret({ format: "api-key" });
|
|
770
|
+
setSecret(entry.key, newValue, {
|
|
771
|
+
scope: entry.scope,
|
|
772
|
+
projectPath: cfg.projectPaths[0],
|
|
773
|
+
source: "agent"
|
|
774
|
+
});
|
|
775
|
+
report.rotated.push(entry.key);
|
|
776
|
+
logAudit({
|
|
777
|
+
action: "write",
|
|
778
|
+
key: entry.key,
|
|
779
|
+
scope: entry.scope,
|
|
780
|
+
source: "agent",
|
|
781
|
+
detail: "auto-rotated by agent (expired)"
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
} else if (decay.isStale) {
|
|
785
|
+
report.stale++;
|
|
786
|
+
report.warnings.push(
|
|
787
|
+
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
788
|
+
);
|
|
789
|
+
} else {
|
|
790
|
+
report.healthy++;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const anomalies = detectAnomalies();
|
|
794
|
+
report.anomalies = anomalies.length;
|
|
795
|
+
for (const a of anomalies) {
|
|
796
|
+
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
797
|
+
}
|
|
798
|
+
return report;
|
|
799
|
+
}
|
|
800
|
+
function formatReport(report, verbose) {
|
|
801
|
+
const lines = [];
|
|
802
|
+
lines.push(
|
|
803
|
+
`${c.bold(`${SYMBOLS.shield} q-ring agent scan`)} ${c.dim(report.timestamp)}`
|
|
804
|
+
);
|
|
805
|
+
lines.push(
|
|
806
|
+
` ${c.dim("secrets:")} ${report.totalSecrets} ${c.green(`${SYMBOLS.check} ${report.healthy}`)} ${c.yellow(`${SYMBOLS.warning} ${report.stale}`)} ${c.red(`${SYMBOLS.cross} ${report.expired}`)} ${c.dim(`anomalies: ${report.anomalies}`)}`
|
|
807
|
+
);
|
|
808
|
+
if (report.rotated.length > 0) {
|
|
809
|
+
lines.push(
|
|
810
|
+
` ${c.cyan(`${SYMBOLS.zap} auto-rotated:`)} ${report.rotated.join(", ")}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (verbose && report.warnings.length > 0) {
|
|
814
|
+
lines.push("");
|
|
815
|
+
for (const w of report.warnings) {
|
|
816
|
+
if (w.startsWith("EXPIRED")) lines.push(` ${c.red(w)}`);
|
|
817
|
+
else if (w.startsWith("STALE")) lines.push(` ${c.yellow(w)}`);
|
|
818
|
+
else if (w.startsWith("ANOMALY")) lines.push(` ${c.magenta(w)}`);
|
|
819
|
+
else lines.push(` ${w}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return lines.join("\n");
|
|
823
|
+
}
|
|
824
|
+
async function startAgent(config = {}) {
|
|
825
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
826
|
+
console.log(
|
|
827
|
+
`${c.bold(`${SYMBOLS.zap} q-ring agent started`)} ${c.dim(`(interval: ${cfg.intervalSeconds}s, auto-rotate: ${cfg.autoRotate})`)}`
|
|
828
|
+
);
|
|
829
|
+
console.log(
|
|
830
|
+
c.dim(` monitoring: global + ${cfg.projectPaths.length} project(s)`)
|
|
831
|
+
);
|
|
832
|
+
console.log();
|
|
833
|
+
const scan = () => {
|
|
834
|
+
const report = runHealthScan(cfg);
|
|
835
|
+
console.log(formatReport(report, cfg.verbose));
|
|
836
|
+
if (report.warnings.length > 0 || cfg.verbose) {
|
|
837
|
+
console.log();
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
scan();
|
|
841
|
+
const interval = setInterval(scan, cfg.intervalSeconds * 1e3);
|
|
842
|
+
const shutdown = () => {
|
|
843
|
+
clearInterval(interval);
|
|
844
|
+
console.log(`
|
|
845
|
+
${c.dim("q-ring agent stopped")}`);
|
|
846
|
+
process.exit(0);
|
|
847
|
+
};
|
|
848
|
+
process.on("SIGINT", shutdown);
|
|
849
|
+
process.on("SIGTERM", shutdown);
|
|
850
|
+
await new Promise(() => {
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/core/tunnel.ts
|
|
855
|
+
var tunnelStore = /* @__PURE__ */ new Map();
|
|
856
|
+
var cleanupInterval = null;
|
|
857
|
+
function ensureCleanup() {
|
|
858
|
+
if (cleanupInterval) return;
|
|
859
|
+
cleanupInterval = setInterval(() => {
|
|
860
|
+
const now = Date.now();
|
|
861
|
+
for (const [id, entry] of tunnelStore) {
|
|
862
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
863
|
+
tunnelStore.delete(id);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (tunnelStore.size === 0 && cleanupInterval) {
|
|
867
|
+
clearInterval(cleanupInterval);
|
|
868
|
+
cleanupInterval = null;
|
|
869
|
+
}
|
|
870
|
+
}, 5e3);
|
|
871
|
+
if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
|
|
872
|
+
cleanupInterval.unref();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function tunnelCreate(value, opts = {}) {
|
|
876
|
+
const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
877
|
+
const now = Date.now();
|
|
878
|
+
tunnelStore.set(id, {
|
|
879
|
+
value,
|
|
880
|
+
createdAt: now,
|
|
881
|
+
expiresAt: opts.ttlSeconds ? now + opts.ttlSeconds * 1e3 : void 0,
|
|
882
|
+
accessCount: 0,
|
|
883
|
+
maxReads: opts.maxReads
|
|
884
|
+
});
|
|
885
|
+
ensureCleanup();
|
|
886
|
+
return id;
|
|
887
|
+
}
|
|
888
|
+
function tunnelRead(id) {
|
|
889
|
+
const entry = tunnelStore.get(id);
|
|
890
|
+
if (!entry) return null;
|
|
891
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
892
|
+
tunnelStore.delete(id);
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
entry.accessCount++;
|
|
896
|
+
if (entry.maxReads && entry.accessCount >= entry.maxReads) {
|
|
897
|
+
const value = entry.value;
|
|
898
|
+
tunnelStore.delete(id);
|
|
899
|
+
return value;
|
|
900
|
+
}
|
|
901
|
+
return entry.value;
|
|
902
|
+
}
|
|
903
|
+
function tunnelDestroy(id) {
|
|
904
|
+
return tunnelStore.delete(id);
|
|
905
|
+
}
|
|
906
|
+
function tunnelList() {
|
|
907
|
+
const now = Date.now();
|
|
908
|
+
const result = [];
|
|
909
|
+
for (const [id, entry] of tunnelStore) {
|
|
910
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
911
|
+
tunnelStore.delete(id);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
result.push({
|
|
915
|
+
id,
|
|
916
|
+
createdAt: entry.createdAt,
|
|
917
|
+
expiresAt: entry.expiresAt,
|
|
918
|
+
accessCount: entry.accessCount,
|
|
919
|
+
maxReads: entry.maxReads
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/core/teleport.ts
|
|
926
|
+
import {
|
|
927
|
+
randomBytes as randomBytes2,
|
|
928
|
+
createCipheriv,
|
|
929
|
+
createDecipheriv,
|
|
930
|
+
pbkdf2Sync
|
|
931
|
+
} from "crypto";
|
|
932
|
+
var ALGORITHM = "aes-256-gcm";
|
|
933
|
+
var KEY_LENGTH = 32;
|
|
934
|
+
var IV_LENGTH = 16;
|
|
935
|
+
var SALT_LENGTH = 32;
|
|
936
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
937
|
+
function deriveKey(passphrase, salt) {
|
|
938
|
+
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
939
|
+
}
|
|
940
|
+
function teleportPack(secrets, passphrase) {
|
|
941
|
+
const payload = {
|
|
942
|
+
secrets,
|
|
943
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
944
|
+
};
|
|
945
|
+
const plaintext = JSON.stringify(payload);
|
|
946
|
+
const salt = randomBytes2(SALT_LENGTH);
|
|
947
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
948
|
+
const key = deriveKey(passphrase, salt);
|
|
949
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
950
|
+
const encrypted = Buffer.concat([
|
|
951
|
+
cipher.update(plaintext, "utf8"),
|
|
952
|
+
cipher.final()
|
|
953
|
+
]);
|
|
954
|
+
const tag = cipher.getAuthTag();
|
|
955
|
+
const bundle = {
|
|
956
|
+
v: 1,
|
|
957
|
+
data: encrypted.toString("base64"),
|
|
958
|
+
salt: salt.toString("base64"),
|
|
959
|
+
iv: iv.toString("base64"),
|
|
960
|
+
tag: tag.toString("base64"),
|
|
961
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
|
+
count: secrets.length
|
|
963
|
+
};
|
|
964
|
+
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
965
|
+
}
|
|
966
|
+
function teleportUnpack(encoded, passphrase) {
|
|
967
|
+
const bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
968
|
+
const bundle = JSON.parse(bundleJson);
|
|
969
|
+
if (bundle.v !== 1) {
|
|
970
|
+
throw new Error(`Unsupported teleport bundle version: ${bundle.v}`);
|
|
971
|
+
}
|
|
972
|
+
const salt = Buffer.from(bundle.salt, "base64");
|
|
973
|
+
const iv = Buffer.from(bundle.iv, "base64");
|
|
974
|
+
const tag = Buffer.from(bundle.tag, "base64");
|
|
975
|
+
const encrypted = Buffer.from(bundle.data, "base64");
|
|
976
|
+
const key = deriveKey(passphrase, salt);
|
|
977
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
978
|
+
decipher.setAuthTag(tag);
|
|
979
|
+
const decrypted = Buffer.concat([
|
|
980
|
+
decipher.update(encrypted),
|
|
981
|
+
decipher.final()
|
|
982
|
+
]);
|
|
983
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/utils/prompt.ts
|
|
987
|
+
import { createInterface } from "readline";
|
|
988
|
+
async function promptSecret(message) {
|
|
989
|
+
const rl = createInterface({
|
|
990
|
+
input: process.stdin,
|
|
991
|
+
output: process.stderr,
|
|
992
|
+
terminal: true
|
|
993
|
+
});
|
|
994
|
+
return new Promise((resolve) => {
|
|
995
|
+
process.stderr.write(message);
|
|
996
|
+
const stdin = process.stdin;
|
|
997
|
+
const wasRaw = stdin.isRaw;
|
|
998
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
999
|
+
stdin.setRawMode(true);
|
|
1000
|
+
}
|
|
1001
|
+
let input = "";
|
|
1002
|
+
const onData = (chunk) => {
|
|
1003
|
+
const char = chunk.toString("utf8");
|
|
1004
|
+
if (char === "\n" || char === "\r") {
|
|
1005
|
+
stdin.removeListener("data", onData);
|
|
1006
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
1007
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
1008
|
+
}
|
|
1009
|
+
process.stderr.write("\n");
|
|
1010
|
+
rl.close();
|
|
1011
|
+
resolve(input);
|
|
1012
|
+
} else if (char === "") {
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
} else if (char === "\x7F" || char === "\b") {
|
|
1015
|
+
input = input.slice(0, -1);
|
|
1016
|
+
} else {
|
|
1017
|
+
input += char;
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
stdin.on("data", onData);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/cli/commands.ts
|
|
1025
|
+
function buildOpts(cmd) {
|
|
1026
|
+
let scope;
|
|
1027
|
+
if (cmd.global) scope = "global";
|
|
1028
|
+
else if (cmd.project) scope = "project";
|
|
1029
|
+
const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
|
|
1030
|
+
if (scope === "project" && !projectPath) {
|
|
1031
|
+
throw new Error("Project path is required for project scope");
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
scope,
|
|
1035
|
+
projectPath: projectPath ?? process.cwd(),
|
|
1036
|
+
env: cmd.env,
|
|
1037
|
+
source: "cli"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function createProgram() {
|
|
1041
|
+
const program2 = new Command().name("qring").description(
|
|
1042
|
+
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
1043
|
+
).version("0.2.0");
|
|
1044
|
+
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").action(async (key, value, cmd) => {
|
|
1045
|
+
const opts = buildOpts(cmd);
|
|
1046
|
+
if (!value) {
|
|
1047
|
+
value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
|
|
1048
|
+
if (!value) {
|
|
1049
|
+
console.error(c.red(`${SYMBOLS.cross} No value provided, aborting.`));
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const setOpts = {
|
|
1054
|
+
...opts,
|
|
1055
|
+
ttlSeconds: cmd.ttl,
|
|
1056
|
+
expiresAt: cmd.expires,
|
|
1057
|
+
description: cmd.description,
|
|
1058
|
+
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
1059
|
+
};
|
|
1060
|
+
if (cmd.env) {
|
|
1061
|
+
const existing = getEnvelope(key, opts);
|
|
1062
|
+
const states = existing?.envelope?.states ?? {};
|
|
1063
|
+
states[cmd.env] = value;
|
|
1064
|
+
if (existing?.envelope?.value && !states["default"]) {
|
|
1065
|
+
states["default"] = existing.envelope.value;
|
|
1066
|
+
}
|
|
1067
|
+
setOpts.states = states;
|
|
1068
|
+
setOpts.defaultEnv = existing?.envelope?.defaultEnv ?? cmd.env;
|
|
1069
|
+
setSecret(key, "", setOpts);
|
|
1070
|
+
console.log(
|
|
1071
|
+
`${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${envBadge(cmd.env)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)}`
|
|
1072
|
+
);
|
|
1073
|
+
} else {
|
|
1074
|
+
setSecret(key, value, setOpts);
|
|
1075
|
+
const extras = [];
|
|
1076
|
+
if (cmd.ttl) extras.push(`${SYMBOLS.clock} ttl=${cmd.ttl}s`);
|
|
1077
|
+
if (cmd.description) extras.push(c.dim(cmd.description));
|
|
1078
|
+
console.log(
|
|
1079
|
+
`${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)} ${extras.join(" ")}`
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
program2.command("get <key>").description("Retrieve a secret (collapses superposition if needed)").option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force a specific environment").action((key, cmd) => {
|
|
1084
|
+
const opts = buildOpts(cmd);
|
|
1085
|
+
const value = getSecret(key, opts);
|
|
1086
|
+
if (value === null) {
|
|
1087
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
process.stdout.write(value);
|
|
1091
|
+
});
|
|
1092
|
+
program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
1093
|
+
const opts = buildOpts(cmd);
|
|
1094
|
+
const deleted = deleteSecret(key, opts);
|
|
1095
|
+
if (deleted) {
|
|
1096
|
+
console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
|
|
1097
|
+
} else {
|
|
1098
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").action((cmd) => {
|
|
1103
|
+
const opts = buildOpts(cmd);
|
|
1104
|
+
const entries = listSecrets(opts);
|
|
1105
|
+
if (entries.length === 0) {
|
|
1106
|
+
console.log(c.dim("No secrets found"));
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
console.log(
|
|
1110
|
+
c.bold(`
|
|
1111
|
+
${SYMBOLS.key} q-ring secrets (${entries.length})
|
|
1112
|
+
`)
|
|
1113
|
+
);
|
|
1114
|
+
const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
|
|
1115
|
+
for (const entry of entries) {
|
|
1116
|
+
const parts = [];
|
|
1117
|
+
parts.push(c.dim("[") + scopeColor(entry.scope) + c.dim("]"));
|
|
1118
|
+
parts.push(c.bold(entry.key.padEnd(maxKeyLen)));
|
|
1119
|
+
if (entry.envelope?.states) {
|
|
1120
|
+
const envs = Object.keys(entry.envelope.states);
|
|
1121
|
+
parts.push(c.magenta(`[${envs.join("|")}]`));
|
|
1122
|
+
}
|
|
1123
|
+
if (entry.decay && (entry.decay.lifetimePercent > 0 || entry.decay.isExpired)) {
|
|
1124
|
+
parts.push(
|
|
1125
|
+
decayIndicator(entry.decay.lifetimePercent, entry.decay.isExpired)
|
|
1126
|
+
);
|
|
1127
|
+
if (entry.decay.timeRemaining && !entry.decay.isExpired) {
|
|
1128
|
+
parts.push(c.dim(entry.decay.timeRemaining));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (entry.envelope?.meta.entangled && entry.envelope.meta.entangled.length > 0) {
|
|
1132
|
+
parts.push(
|
|
1133
|
+
c.cyan(`${SYMBOLS.link} ${entry.envelope.meta.entangled.length}`)
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (entry.envelope && entry.envelope.meta.accessCount > 0) {
|
|
1137
|
+
parts.push(
|
|
1138
|
+
c.dim(`${SYMBOLS.eye} ${entry.envelope.meta.accessCount}`)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (entry.envelope?.meta.tags && entry.envelope.meta.tags.length > 0) {
|
|
1142
|
+
parts.push(
|
|
1143
|
+
c.dim(entry.envelope.meta.tags.map((t) => `#${t}`).join(" "))
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
console.log(` ${parts.join(" ")}`);
|
|
1147
|
+
}
|
|
1148
|
+
console.log();
|
|
1149
|
+
});
|
|
1150
|
+
program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
1151
|
+
const opts = buildOpts(cmd);
|
|
1152
|
+
const result = getEnvelope(key, opts);
|
|
1153
|
+
if (!result) {
|
|
1154
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
const { envelope, scope } = result;
|
|
1158
|
+
const decay = checkDecay(envelope);
|
|
1159
|
+
console.log(`
|
|
1160
|
+
${c.bold(SYMBOLS.key + " " + key)}`);
|
|
1161
|
+
console.log(` ${c.dim("scope:")} ${scopeColor(scope)}`);
|
|
1162
|
+
if (envelope.states) {
|
|
1163
|
+
console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
|
|
1164
|
+
console.log(` ${c.dim("states:")}`);
|
|
1165
|
+
for (const [env, _] of Object.entries(envelope.states)) {
|
|
1166
|
+
const isDefault = env === envelope.defaultEnv;
|
|
1167
|
+
console.log(
|
|
1168
|
+
` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
|
|
1173
|
+
}
|
|
1174
|
+
console.log(` ${c.dim("created:")} ${envelope.meta.createdAt}`);
|
|
1175
|
+
console.log(` ${c.dim("updated:")} ${envelope.meta.updatedAt}`);
|
|
1176
|
+
console.log(
|
|
1177
|
+
` ${c.dim("accessed:")} ${envelope.meta.accessCount} times`
|
|
1178
|
+
);
|
|
1179
|
+
if (envelope.meta.lastAccessedAt) {
|
|
1180
|
+
console.log(
|
|
1181
|
+
` ${c.dim("last read:")} ${envelope.meta.lastAccessedAt}`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
if (envelope.meta.description) {
|
|
1185
|
+
console.log(` ${c.dim("desc:")} ${envelope.meta.description}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (envelope.meta.tags && envelope.meta.tags.length > 0) {
|
|
1188
|
+
console.log(
|
|
1189
|
+
` ${c.dim("tags:")} ${envelope.meta.tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (decay.timeRemaining) {
|
|
1193
|
+
console.log(
|
|
1194
|
+
` ${c.dim("decay:")} ${decayIndicator(decay.lifetimePercent, decay.isExpired)} ${decay.timeRemaining}`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (envelope.meta.entangled && envelope.meta.entangled.length > 0) {
|
|
1198
|
+
console.log(` ${c.dim("entangled:")}`);
|
|
1199
|
+
for (const link of envelope.meta.entangled) {
|
|
1200
|
+
console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
console.log();
|
|
1204
|
+
});
|
|
1205
|
+
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").action((cmd) => {
|
|
1206
|
+
const opts = buildOpts(cmd);
|
|
1207
|
+
const output = exportSecrets({ ...opts, format: cmd.format });
|
|
1208
|
+
process.stdout.write(output + "\n");
|
|
1209
|
+
});
|
|
1210
|
+
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
1211
|
+
const result = collapseEnvironment({
|
|
1212
|
+
projectPath: cmd.projectPath ?? process.cwd()
|
|
1213
|
+
});
|
|
1214
|
+
if (result) {
|
|
1215
|
+
console.log(
|
|
1216
|
+
`${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
|
|
1217
|
+
);
|
|
1218
|
+
} else {
|
|
1219
|
+
console.log(
|
|
1220
|
+
c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
|
|
1225
|
+
"-f, --format <format>",
|
|
1226
|
+
"Format: hex, base64, alphanumeric, uuid, api-key, token, password",
|
|
1227
|
+
"api-key"
|
|
1228
|
+
).option("-l, --length <n>", "Length (bytes or chars depending on format)", parseInt).option("--prefix <prefix>", "Prefix for api-key/token format").option("-s, --save <key>", "Save the generated secret to keyring with this key").option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
1229
|
+
const secret = generateSecret({
|
|
1230
|
+
format: cmd.format,
|
|
1231
|
+
length: cmd.length,
|
|
1232
|
+
prefix: cmd.prefix
|
|
1233
|
+
});
|
|
1234
|
+
const entropy = estimateEntropy(secret);
|
|
1235
|
+
if (cmd.save) {
|
|
1236
|
+
const opts = buildOpts(cmd);
|
|
1237
|
+
setSecret(cmd.save, secret, opts);
|
|
1238
|
+
console.log(
|
|
1239
|
+
`${SYMBOLS.sparkle} ${c.green("generated & saved")} ${c.bold(cmd.save)} ${c.dim(`(${cmd.format}, ${entropy} bits entropy)`)}`
|
|
1240
|
+
);
|
|
1241
|
+
} else {
|
|
1242
|
+
process.stdout.write(secret);
|
|
1243
|
+
if (process.stdout.isTTY) {
|
|
1244
|
+
console.log(
|
|
1245
|
+
`
|
|
1246
|
+
${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
program2.command("entangle <sourceKey> <targetKey>").description("Link two secrets \u2014 rotating one updates the other").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
|
|
1252
|
+
(sourceKey, targetKey, cmd) => {
|
|
1253
|
+
const sourceOpts = {
|
|
1254
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
1255
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
1256
|
+
source: "cli"
|
|
1257
|
+
};
|
|
1258
|
+
const targetOpts = {
|
|
1259
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
1260
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
1261
|
+
source: "cli"
|
|
1262
|
+
};
|
|
1263
|
+
entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
1264
|
+
console.log(
|
|
1265
|
+
`${SYMBOLS.link} ${c.cyan("entangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
|
|
1270
|
+
tunnel.command("create <value>").description("Create a tunneled secret (returns tunnel ID)").option("--ttl <seconds>", "Auto-expire after N seconds", parseInt).option("--max-reads <n>", "Self-destruct after N reads", parseInt).action((value, cmd) => {
|
|
1271
|
+
const id = tunnelCreate(value, {
|
|
1272
|
+
ttlSeconds: cmd.ttl,
|
|
1273
|
+
maxReads: cmd.maxReads
|
|
1274
|
+
});
|
|
1275
|
+
console.log(`${SYMBOLS.ghost} ${c.magenta("tunneled")} ${c.bold(id)}`);
|
|
1276
|
+
const extras = [];
|
|
1277
|
+
if (cmd.ttl) extras.push(`ttl=${cmd.ttl}s`);
|
|
1278
|
+
if (cmd.maxReads) extras.push(`max-reads=${cmd.maxReads}`);
|
|
1279
|
+
if (extras.length) console.log(c.dim(` ${extras.join(" | ")}`));
|
|
1280
|
+
});
|
|
1281
|
+
tunnel.command("read <id>").description("Read a tunneled secret (may self-destruct)").action((id) => {
|
|
1282
|
+
const value = tunnelRead(id);
|
|
1283
|
+
if (value === null) {
|
|
1284
|
+
console.error(
|
|
1285
|
+
c.red(`${SYMBOLS.cross} Tunnel "${id}" not found or expired`)
|
|
1286
|
+
);
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
process.stdout.write(value);
|
|
1290
|
+
});
|
|
1291
|
+
tunnel.command("destroy <id>").description("Destroy a tunneled secret immediately").action((id) => {
|
|
1292
|
+
const destroyed = tunnelDestroy(id);
|
|
1293
|
+
if (destroyed) {
|
|
1294
|
+
console.log(`${SYMBOLS.check} ${c.green("destroyed")} ${id}`);
|
|
1295
|
+
} else {
|
|
1296
|
+
console.error(
|
|
1297
|
+
c.red(`${SYMBOLS.cross} Tunnel "${id}" not found`)
|
|
1298
|
+
);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
tunnel.command("list").alias("ls").description("List active tunnels").action(() => {
|
|
1303
|
+
const tunnels = tunnelList();
|
|
1304
|
+
if (tunnels.length === 0) {
|
|
1305
|
+
console.log(c.dim("No active tunnels"));
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
console.log(c.bold(`
|
|
1309
|
+
${SYMBOLS.ghost} Active tunnels (${tunnels.length})
|
|
1310
|
+
`));
|
|
1311
|
+
for (const t of tunnels) {
|
|
1312
|
+
const parts = [c.bold(t.id)];
|
|
1313
|
+
parts.push(c.dim(`reads: ${t.accessCount}`));
|
|
1314
|
+
if (t.maxReads) parts.push(c.dim(`max: ${t.maxReads}`));
|
|
1315
|
+
if (t.expiresAt) {
|
|
1316
|
+
const remaining = Math.max(0, Math.floor((t.expiresAt - Date.now()) / 1e3));
|
|
1317
|
+
parts.push(c.dim(`expires: ${remaining}s`));
|
|
1318
|
+
}
|
|
1319
|
+
console.log(` ${SYMBOLS.ghost} ${parts.join(" ")}`);
|
|
1320
|
+
}
|
|
1321
|
+
console.log();
|
|
1322
|
+
});
|
|
1323
|
+
const tp = program2.command("teleport").alias("tp").description("Encrypted secret sharing (quantum teleportation)");
|
|
1324
|
+
tp.command("pack").description("Pack secrets into an encrypted bundle").option("-k, --keys <keys>", "Comma-separated key names to pack").option("-g, --global", "Pack global scope").option("-p, --project", "Pack project scope").option("--project-path <path>", "Explicit project path").action(async (cmd) => {
|
|
1325
|
+
const opts = buildOpts(cmd);
|
|
1326
|
+
const passphrase = await promptSecret(
|
|
1327
|
+
`${SYMBOLS.lock} Enter passphrase for encryption: `
|
|
1328
|
+
);
|
|
1329
|
+
if (!passphrase) {
|
|
1330
|
+
console.error(c.red("Passphrase required"));
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
const entries = listSecrets(opts);
|
|
1334
|
+
let keys;
|
|
1335
|
+
if (cmd.keys) {
|
|
1336
|
+
keys = cmd.keys.split(",").map((k) => k.trim());
|
|
1337
|
+
}
|
|
1338
|
+
const secrets = [];
|
|
1339
|
+
for (const entry of entries) {
|
|
1340
|
+
if (keys && !keys.includes(entry.key)) continue;
|
|
1341
|
+
const value = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1342
|
+
if (value !== null) {
|
|
1343
|
+
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (secrets.length === 0) {
|
|
1347
|
+
console.error(c.red("No secrets to pack"));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
const bundle = teleportPack(secrets, passphrase);
|
|
1351
|
+
process.stdout.write(bundle);
|
|
1352
|
+
if (process.stdout.isTTY) {
|
|
1353
|
+
console.log(
|
|
1354
|
+
`
|
|
1355
|
+
${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").action(async (bundle, cmd) => {
|
|
1360
|
+
if (!bundle) {
|
|
1361
|
+
const chunks = [];
|
|
1362
|
+
for await (const chunk of process.stdin) {
|
|
1363
|
+
chunks.push(chunk);
|
|
1364
|
+
}
|
|
1365
|
+
bundle = Buffer.concat(chunks).toString("utf8").trim();
|
|
1366
|
+
}
|
|
1367
|
+
const passphrase = await promptSecret(
|
|
1368
|
+
`${SYMBOLS.lock} Enter passphrase for decryption: `
|
|
1369
|
+
);
|
|
1370
|
+
try {
|
|
1371
|
+
const payload = teleportUnpack(bundle, passphrase);
|
|
1372
|
+
if (cmd.dryRun) {
|
|
1373
|
+
console.log(
|
|
1374
|
+
`
|
|
1375
|
+
${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
|
|
1376
|
+
`
|
|
1377
|
+
);
|
|
1378
|
+
for (const s of payload.secrets) {
|
|
1379
|
+
console.log(` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`);
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const opts = buildOpts(cmd);
|
|
1384
|
+
for (const s of payload.secrets) {
|
|
1385
|
+
setSecret(s.key, s.value, opts);
|
|
1386
|
+
}
|
|
1387
|
+
console.log(
|
|
1388
|
+
`${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
|
|
1389
|
+
);
|
|
1390
|
+
} catch {
|
|
1391
|
+
console.error(
|
|
1392
|
+
c.red(`${SYMBOLS.cross} Failed to unpack: wrong passphrase or corrupted bundle`)
|
|
1393
|
+
);
|
|
1394
|
+
process.exit(1);
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
program2.command("audit").description("View the audit log (observer effect)").option("-k, --key <key>", "Filter by key").option(
|
|
1398
|
+
"-a, --action <action>",
|
|
1399
|
+
"Filter by action (read, write, delete, etc.)"
|
|
1400
|
+
).option("-n, --limit <n>", "Number of events to show", parseInt, 20).option("--anomalies", "Detect access anomalies").action((cmd) => {
|
|
1401
|
+
if (cmd.anomalies) {
|
|
1402
|
+
const anomalies = detectAnomalies(cmd.key);
|
|
1403
|
+
if (anomalies.length === 0) {
|
|
1404
|
+
console.log(
|
|
1405
|
+
`${SYMBOLS.shield} ${c.green("No anomalies detected")}`
|
|
1406
|
+
);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
console.log(
|
|
1410
|
+
`
|
|
1411
|
+
${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies detected`))}
|
|
1412
|
+
`
|
|
1413
|
+
);
|
|
1414
|
+
for (const a of anomalies) {
|
|
1415
|
+
console.log(` ${c.yellow(a.type)} ${a.description}`);
|
|
1416
|
+
}
|
|
1417
|
+
console.log();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const events = queryAudit({
|
|
1421
|
+
key: cmd.key,
|
|
1422
|
+
action: cmd.action,
|
|
1423
|
+
limit: cmd.limit
|
|
1424
|
+
});
|
|
1425
|
+
if (events.length === 0) {
|
|
1426
|
+
console.log(c.dim("No audit events found"));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
console.log(
|
|
1430
|
+
c.bold(`
|
|
1431
|
+
${SYMBOLS.eye} Audit log (${events.length} events)
|
|
1432
|
+
`)
|
|
1433
|
+
);
|
|
1434
|
+
for (const event of events) {
|
|
1435
|
+
const ts = new Date(event.timestamp).toLocaleString();
|
|
1436
|
+
const actionColor = event.action === "read" ? c.blue : event.action === "write" ? c.green : event.action === "delete" ? c.red : c.yellow;
|
|
1437
|
+
const parts = [
|
|
1438
|
+
c.dim(ts),
|
|
1439
|
+
actionColor(event.action.padEnd(8)),
|
|
1440
|
+
event.key ? c.bold(event.key) : "",
|
|
1441
|
+
event.scope ? c.dim(`[${event.scope}]`) : "",
|
|
1442
|
+
event.detail ? c.dim(event.detail) : ""
|
|
1443
|
+
];
|
|
1444
|
+
console.log(` ${parts.filter(Boolean).join(" ")}`);
|
|
1445
|
+
}
|
|
1446
|
+
console.log();
|
|
1447
|
+
});
|
|
1448
|
+
program2.command("health").description("Check the health of all secrets").option("-g, --global", "Check global scope only").option("-p, --project", "Check project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
1449
|
+
const opts = buildOpts(cmd);
|
|
1450
|
+
const entries = listSecrets(opts);
|
|
1451
|
+
if (entries.length === 0) {
|
|
1452
|
+
console.log(c.dim("No secrets to check"));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
console.log(
|
|
1456
|
+
c.bold(`
|
|
1457
|
+
${SYMBOLS.shield} Secret health report
|
|
1458
|
+
`)
|
|
1459
|
+
);
|
|
1460
|
+
let healthy = 0;
|
|
1461
|
+
let stale = 0;
|
|
1462
|
+
let expired = 0;
|
|
1463
|
+
let noDecay = 0;
|
|
1464
|
+
for (const entry of entries) {
|
|
1465
|
+
if (!entry.decay || !entry.decay.timeRemaining) {
|
|
1466
|
+
noDecay++;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (entry.decay.isExpired) {
|
|
1470
|
+
expired++;
|
|
1471
|
+
console.log(
|
|
1472
|
+
` ${c.red(SYMBOLS.cross)} ${c.bold(entry.key)} ${c.bgRed(c.white(" EXPIRED "))}`
|
|
1473
|
+
);
|
|
1474
|
+
} else if (entry.decay.isStale) {
|
|
1475
|
+
stale++;
|
|
1476
|
+
console.log(
|
|
1477
|
+
` ${c.yellow(SYMBOLS.warning)} ${c.bold(entry.key)} ${c.yellow(`stale (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`)}`
|
|
1478
|
+
);
|
|
1479
|
+
} else {
|
|
1480
|
+
healthy++;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
console.log(
|
|
1484
|
+
`
|
|
1485
|
+
${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
|
|
1486
|
+
);
|
|
1487
|
+
const anomalies = detectAnomalies();
|
|
1488
|
+
if (anomalies.length > 0) {
|
|
1489
|
+
console.log(
|
|
1490
|
+
`
|
|
1491
|
+
${c.yellow(`${SYMBOLS.warning} ${anomalies.length} access anomaly/anomalies detected`)} ${c.dim("(run: qring audit --anomalies)")}`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
console.log();
|
|
1495
|
+
});
|
|
1496
|
+
program2.command("agent").description("Start the autonomous agent (background monitor)").option(
|
|
1497
|
+
"-i, --interval <seconds>",
|
|
1498
|
+
"Scan interval in seconds",
|
|
1499
|
+
parseInt,
|
|
1500
|
+
60
|
|
1501
|
+
).option("--auto-rotate", "Auto-rotate expired secrets").option("-v, --verbose", "Verbose output with all warnings").option("--project-path <paths>", "Comma-separated project paths to monitor").option("--once", "Run a single scan and exit (no daemon)").action(async (cmd) => {
|
|
1502
|
+
const projectPaths = cmd.projectPath ? cmd.projectPath.split(",").map((p) => p.trim()) : [process.cwd()];
|
|
1503
|
+
if (cmd.once) {
|
|
1504
|
+
const report = runHealthScan({
|
|
1505
|
+
autoRotate: cmd.autoRotate,
|
|
1506
|
+
projectPaths,
|
|
1507
|
+
verbose: cmd.verbose
|
|
1508
|
+
});
|
|
1509
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
await startAgent({
|
|
1513
|
+
intervalSeconds: cmd.interval,
|
|
1514
|
+
autoRotate: cmd.autoRotate,
|
|
1515
|
+
projectPaths,
|
|
1516
|
+
verbose: cmd.verbose
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
return program2;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/index.ts
|
|
1523
|
+
var program = createProgram();
|
|
1524
|
+
program.parse();
|
|
1525
|
+
//# sourceMappingURL=index.js.map
|