@i4ctime/q-ring 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/chunk-3WTTWJYU.js +653 -0
- package/dist/chunk-3WTTWJYU.js.map +1 -0
- package/dist/chunk-F4SPZ774.js +675 -0
- package/dist/chunk-F4SPZ774.js.map +1 -0
- package/dist/dashboard-QQWKOOI5.js +532 -0
- package/dist/dashboard-QQWKOOI5.js.map +1 -0
- package/dist/dashboard-X3ONQFLV.js +532 -0
- package/dist/dashboard-X3ONQFLV.js.map +1 -0
- package/dist/index.js +48 -648
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +35 -626
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,6 +185,23 @@ qring agent --auto-rotate
|
|
|
185
185
|
qring agent --once
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
### Quantum Status Dashboard — Live Monitoring
|
|
189
|
+
|
|
190
|
+
Launch a real-time dashboard in your browser that visualizes every quantum subsystem at a glance: health summary, decay timers, superposition states, entanglement pairs, active tunnels, anomaly alerts, audit log, and environment detection.
|
|
191
|
+
|
|
192
|
+
The dashboard is a self-contained HTML page served locally. Data streams in via Server-Sent Events and updates every 5 seconds — no dependencies, no cloud, no config.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Open the dashboard (auto-launches your browser)
|
|
196
|
+
qring status
|
|
197
|
+
|
|
198
|
+
# Specify a custom port
|
|
199
|
+
qring status --port 4200
|
|
200
|
+
|
|
201
|
+
# Don't auto-open the browser
|
|
202
|
+
qring status --no-open
|
|
203
|
+
```
|
|
204
|
+
|
|
188
205
|
## MCP Server
|
|
189
206
|
|
|
190
207
|
q-ring includes a full MCP server with 20 tools for AI agent integration.
|
|
@@ -305,7 +322,8 @@ MCP Server ────┘ │
|
|
|
305
322
|
├── Entanglement (cross-secret linking)
|
|
306
323
|
├── Tunnel (ephemeral in-memory)
|
|
307
324
|
├── Teleport (encrypted sharing)
|
|
308
|
-
|
|
325
|
+
├── Agent (autonomous monitor)
|
|
326
|
+
└── Dashboard (live status via SSE)
|
|
309
327
|
```
|
|
310
328
|
|
|
311
329
|
## Project Config (`.q-ring.json`)
|
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/envelope.ts
|
|
4
|
+
function createEnvelope(value, opts) {
|
|
5
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6
|
+
let expiresAt = opts?.expiresAt;
|
|
7
|
+
if (!expiresAt && opts?.ttlSeconds) {
|
|
8
|
+
expiresAt = new Date(Date.now() + opts.ttlSeconds * 1e3).toISOString();
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
v: 1,
|
|
12
|
+
value: opts?.states ? void 0 : value,
|
|
13
|
+
states: opts?.states,
|
|
14
|
+
defaultEnv: opts?.defaultEnv,
|
|
15
|
+
meta: {
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
expiresAt,
|
|
19
|
+
ttlSeconds: opts?.ttlSeconds,
|
|
20
|
+
description: opts?.description,
|
|
21
|
+
tags: opts?.tags,
|
|
22
|
+
entangled: opts?.entangled,
|
|
23
|
+
accessCount: 0
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parseEnvelope(raw) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (parsed && typeof parsed === "object" && parsed.v === 1) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function wrapLegacy(rawValue) {
|
|
38
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
39
|
+
return {
|
|
40
|
+
v: 1,
|
|
41
|
+
value: rawValue,
|
|
42
|
+
meta: {
|
|
43
|
+
createdAt: now,
|
|
44
|
+
updatedAt: now,
|
|
45
|
+
accessCount: 0
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function serializeEnvelope(envelope) {
|
|
50
|
+
return JSON.stringify(envelope);
|
|
51
|
+
}
|
|
52
|
+
function collapseValue(envelope, env) {
|
|
53
|
+
if (envelope.states) {
|
|
54
|
+
const targetEnv = env ?? envelope.defaultEnv;
|
|
55
|
+
if (targetEnv && envelope.states[targetEnv]) {
|
|
56
|
+
return envelope.states[targetEnv];
|
|
57
|
+
}
|
|
58
|
+
if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
|
|
59
|
+
return envelope.states[envelope.defaultEnv];
|
|
60
|
+
}
|
|
61
|
+
const keys = Object.keys(envelope.states);
|
|
62
|
+
if (keys.length > 0) {
|
|
63
|
+
return envelope.states[keys[0]];
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return envelope.value ?? null;
|
|
68
|
+
}
|
|
69
|
+
function checkDecay(envelope) {
|
|
70
|
+
if (!envelope.meta.expiresAt) {
|
|
71
|
+
return {
|
|
72
|
+
isExpired: false,
|
|
73
|
+
isStale: false,
|
|
74
|
+
lifetimePercent: 0,
|
|
75
|
+
secondsRemaining: null,
|
|
76
|
+
timeRemaining: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
81
|
+
const created = new Date(envelope.meta.createdAt).getTime();
|
|
82
|
+
const totalLifetime = expires - created;
|
|
83
|
+
const elapsed = now - created;
|
|
84
|
+
const remaining = expires - now;
|
|
85
|
+
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
86
|
+
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
87
|
+
let timeRemaining = null;
|
|
88
|
+
if (remaining > 0) {
|
|
89
|
+
const days = Math.floor(remaining / 864e5);
|
|
90
|
+
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
91
|
+
const minutes = Math.floor(remaining % 36e5 / 6e4);
|
|
92
|
+
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
93
|
+
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
94
|
+
else timeRemaining = `${minutes}m`;
|
|
95
|
+
} else {
|
|
96
|
+
timeRemaining = "expired";
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
isExpired: remaining <= 0,
|
|
100
|
+
isStale: lifetimePercent >= 75,
|
|
101
|
+
lifetimePercent,
|
|
102
|
+
secondsRemaining,
|
|
103
|
+
timeRemaining
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function recordAccess(envelope) {
|
|
107
|
+
return {
|
|
108
|
+
...envelope,
|
|
109
|
+
meta: {
|
|
110
|
+
...envelope.meta,
|
|
111
|
+
accessCount: envelope.meta.accessCount + 1,
|
|
112
|
+
lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/core/collapse.ts
|
|
118
|
+
import { execSync } from "child_process";
|
|
119
|
+
import { existsSync, readFileSync } from "fs";
|
|
120
|
+
import { join } from "path";
|
|
121
|
+
var BRANCH_ENV_MAP = {
|
|
122
|
+
main: "prod",
|
|
123
|
+
master: "prod",
|
|
124
|
+
production: "prod",
|
|
125
|
+
develop: "dev",
|
|
126
|
+
development: "dev",
|
|
127
|
+
dev: "dev",
|
|
128
|
+
staging: "staging",
|
|
129
|
+
stage: "staging",
|
|
130
|
+
test: "test",
|
|
131
|
+
testing: "test"
|
|
132
|
+
};
|
|
133
|
+
function detectGitBranch(cwd) {
|
|
134
|
+
try {
|
|
135
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
136
|
+
cwd: cwd ?? process.cwd(),
|
|
137
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
encoding: "utf8",
|
|
139
|
+
timeout: 3e3
|
|
140
|
+
}).trim();
|
|
141
|
+
return branch || null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function readProjectConfig(projectPath) {
|
|
147
|
+
const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
|
|
148
|
+
try {
|
|
149
|
+
if (existsSync(configPath)) {
|
|
150
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function collapseEnvironment(ctx = {}) {
|
|
157
|
+
if (ctx.explicit) {
|
|
158
|
+
return { env: ctx.explicit, source: "explicit" };
|
|
159
|
+
}
|
|
160
|
+
const qringEnv = process.env.QRING_ENV;
|
|
161
|
+
if (qringEnv) {
|
|
162
|
+
return { env: qringEnv, source: "QRING_ENV" };
|
|
163
|
+
}
|
|
164
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
165
|
+
if (nodeEnv) {
|
|
166
|
+
const mapped = mapEnvName(nodeEnv);
|
|
167
|
+
return { env: mapped, source: "NODE_ENV" };
|
|
168
|
+
}
|
|
169
|
+
const config = readProjectConfig(ctx.projectPath);
|
|
170
|
+
if (config?.env) {
|
|
171
|
+
return { env: config.env, source: "project-config" };
|
|
172
|
+
}
|
|
173
|
+
const branch = detectGitBranch(ctx.projectPath);
|
|
174
|
+
if (branch) {
|
|
175
|
+
const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
|
|
176
|
+
const mapped = branchMap[branch];
|
|
177
|
+
if (mapped) {
|
|
178
|
+
return { env: mapped, source: "git-branch" };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (config?.defaultEnv) {
|
|
182
|
+
return { env: config.defaultEnv, source: "project-config" };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function mapEnvName(raw) {
|
|
187
|
+
const lower = raw.toLowerCase();
|
|
188
|
+
if (lower === "production") return "prod";
|
|
189
|
+
if (lower === "development") return "dev";
|
|
190
|
+
return lower;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/core/observer.ts
|
|
194
|
+
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2 } from "fs";
|
|
195
|
+
import { join as join2 } from "path";
|
|
196
|
+
import { homedir } from "os";
|
|
197
|
+
function getAuditDir() {
|
|
198
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
199
|
+
if (!existsSync2(dir)) {
|
|
200
|
+
mkdirSync(dir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
return dir;
|
|
203
|
+
}
|
|
204
|
+
function getAuditPath() {
|
|
205
|
+
return join2(getAuditDir(), "audit.jsonl");
|
|
206
|
+
}
|
|
207
|
+
function logAudit(event) {
|
|
208
|
+
const full = {
|
|
209
|
+
...event,
|
|
210
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
211
|
+
pid: process.pid
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function queryAudit(query = {}) {
|
|
219
|
+
const path = getAuditPath();
|
|
220
|
+
if (!existsSync2(path)) return [];
|
|
221
|
+
try {
|
|
222
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
223
|
+
let events = lines.map((line) => {
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(line);
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}).filter((e) => e !== null);
|
|
230
|
+
if (query.key) {
|
|
231
|
+
events = events.filter((e) => e.key === query.key);
|
|
232
|
+
}
|
|
233
|
+
if (query.action) {
|
|
234
|
+
events = events.filter((e) => e.action === query.action);
|
|
235
|
+
}
|
|
236
|
+
if (query.since) {
|
|
237
|
+
const since = new Date(query.since).getTime();
|
|
238
|
+
events = events.filter(
|
|
239
|
+
(e) => new Date(e.timestamp).getTime() >= since
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
events.sort(
|
|
243
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
244
|
+
);
|
|
245
|
+
if (query.limit) {
|
|
246
|
+
events = events.slice(0, query.limit);
|
|
247
|
+
}
|
|
248
|
+
return events;
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function detectAnomalies(key) {
|
|
254
|
+
const recent = queryAudit({
|
|
255
|
+
key,
|
|
256
|
+
action: "read",
|
|
257
|
+
since: new Date(Date.now() - 36e5).toISOString()
|
|
258
|
+
// last hour
|
|
259
|
+
});
|
|
260
|
+
const anomalies = [];
|
|
261
|
+
if (key && recent.length > 50) {
|
|
262
|
+
anomalies.push({
|
|
263
|
+
type: "burst",
|
|
264
|
+
description: `${recent.length} reads of "${key}" in the last hour`,
|
|
265
|
+
events: recent.slice(0, 10)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const nightAccess = recent.filter((e) => {
|
|
269
|
+
const hour = new Date(e.timestamp).getHours();
|
|
270
|
+
return hour >= 1 && hour < 5;
|
|
271
|
+
});
|
|
272
|
+
if (nightAccess.length > 0) {
|
|
273
|
+
anomalies.push({
|
|
274
|
+
type: "unusual-hour",
|
|
275
|
+
description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
|
|
276
|
+
events: nightAccess
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return anomalies;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/core/entanglement.ts
|
|
283
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
284
|
+
import { join as join3 } from "path";
|
|
285
|
+
import { homedir as homedir2 } from "os";
|
|
286
|
+
function getRegistryPath() {
|
|
287
|
+
const dir = join3(homedir2(), ".config", "q-ring");
|
|
288
|
+
if (!existsSync3(dir)) {
|
|
289
|
+
mkdirSync2(dir, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
return join3(dir, "entanglement.json");
|
|
292
|
+
}
|
|
293
|
+
function loadRegistry() {
|
|
294
|
+
const path = getRegistryPath();
|
|
295
|
+
if (!existsSync3(path)) {
|
|
296
|
+
return { pairs: [] };
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
300
|
+
} catch {
|
|
301
|
+
return { pairs: [] };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function saveRegistry(registry) {
|
|
305
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));
|
|
306
|
+
}
|
|
307
|
+
function entangle(source, target) {
|
|
308
|
+
const registry = loadRegistry();
|
|
309
|
+
const exists = registry.pairs.some(
|
|
310
|
+
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
311
|
+
);
|
|
312
|
+
if (!exists) {
|
|
313
|
+
registry.pairs.push({
|
|
314
|
+
source,
|
|
315
|
+
target,
|
|
316
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
317
|
+
});
|
|
318
|
+
registry.pairs.push({
|
|
319
|
+
source: target,
|
|
320
|
+
target: source,
|
|
321
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
322
|
+
});
|
|
323
|
+
saveRegistry(registry);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function findEntangled(source) {
|
|
327
|
+
const registry = loadRegistry();
|
|
328
|
+
return registry.pairs.filter(
|
|
329
|
+
(p) => p.source.service === source.service && p.source.key === source.key
|
|
330
|
+
).map((p) => p.target);
|
|
331
|
+
}
|
|
332
|
+
function listEntanglements() {
|
|
333
|
+
return loadRegistry().pairs;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/core/keyring.ts
|
|
337
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
338
|
+
|
|
339
|
+
// src/utils/hash.ts
|
|
340
|
+
import { createHash } from "crypto";
|
|
341
|
+
function hashProjectPath(projectPath) {
|
|
342
|
+
return createHash("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/core/scope.ts
|
|
346
|
+
var SERVICE_PREFIX = "q-ring";
|
|
347
|
+
function globalService() {
|
|
348
|
+
return `${SERVICE_PREFIX}:global`;
|
|
349
|
+
}
|
|
350
|
+
function projectService(projectPath) {
|
|
351
|
+
const hash = hashProjectPath(projectPath);
|
|
352
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
353
|
+
}
|
|
354
|
+
function resolveScope(opts) {
|
|
355
|
+
const { scope, projectPath } = opts;
|
|
356
|
+
if (scope === "global") {
|
|
357
|
+
return [{ scope: "global", service: globalService() }];
|
|
358
|
+
}
|
|
359
|
+
if (scope === "project") {
|
|
360
|
+
if (!projectPath) {
|
|
361
|
+
throw new Error("Project path is required for project scope");
|
|
362
|
+
}
|
|
363
|
+
return [
|
|
364
|
+
{ scope: "project", service: projectService(projectPath), projectPath }
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
if (projectPath) {
|
|
368
|
+
return [
|
|
369
|
+
{ scope: "project", service: projectService(projectPath), projectPath },
|
|
370
|
+
{ scope: "global", service: globalService() }
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
return [{ scope: "global", service: globalService() }];
|
|
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 hasSecret(key, opts = {}) {
|
|
501
|
+
const scopes = resolveScope(opts);
|
|
502
|
+
for (const { service } of scopes) {
|
|
503
|
+
const envelope = readEnvelope(service, key);
|
|
504
|
+
if (envelope) {
|
|
505
|
+
const decay = checkDecay(envelope);
|
|
506
|
+
if (!decay.isExpired) return true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
function listSecrets(opts = {}) {
|
|
512
|
+
const source = opts.source ?? "cli";
|
|
513
|
+
const services = [];
|
|
514
|
+
if (!opts.scope || opts.scope === "global") {
|
|
515
|
+
services.push({ service: globalService(), scope: "global" });
|
|
516
|
+
}
|
|
517
|
+
if ((!opts.scope || opts.scope === "project") && opts.projectPath) {
|
|
518
|
+
services.push({
|
|
519
|
+
service: projectService(opts.projectPath),
|
|
520
|
+
scope: "project"
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const results = [];
|
|
524
|
+
const seen = /* @__PURE__ */ new Set();
|
|
525
|
+
for (const { service, scope } of services) {
|
|
526
|
+
try {
|
|
527
|
+
const credentials = findCredentials(service);
|
|
528
|
+
for (const cred of credentials) {
|
|
529
|
+
const id = `${scope}:${cred.account}`;
|
|
530
|
+
if (seen.has(id)) continue;
|
|
531
|
+
seen.add(id);
|
|
532
|
+
const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
|
|
533
|
+
const decay = checkDecay(envelope);
|
|
534
|
+
results.push({
|
|
535
|
+
key: cred.account,
|
|
536
|
+
scope,
|
|
537
|
+
envelope,
|
|
538
|
+
decay
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!opts.silent) {
|
|
545
|
+
logAudit({ action: "list", source });
|
|
546
|
+
}
|
|
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/tunnel.ts
|
|
564
|
+
var tunnelStore = /* @__PURE__ */ new Map();
|
|
565
|
+
var cleanupInterval = null;
|
|
566
|
+
function ensureCleanup() {
|
|
567
|
+
if (cleanupInterval) return;
|
|
568
|
+
cleanupInterval = setInterval(() => {
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
for (const [id, entry] of tunnelStore) {
|
|
571
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
572
|
+
tunnelStore.delete(id);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (tunnelStore.size === 0 && cleanupInterval) {
|
|
576
|
+
clearInterval(cleanupInterval);
|
|
577
|
+
cleanupInterval = null;
|
|
578
|
+
}
|
|
579
|
+
}, 5e3);
|
|
580
|
+
if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
|
|
581
|
+
cleanupInterval.unref();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function tunnelCreate(value, opts = {}) {
|
|
585
|
+
const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
tunnelStore.set(id, {
|
|
588
|
+
value,
|
|
589
|
+
createdAt: now,
|
|
590
|
+
expiresAt: opts.ttlSeconds ? now + opts.ttlSeconds * 1e3 : void 0,
|
|
591
|
+
accessCount: 0,
|
|
592
|
+
maxReads: opts.maxReads
|
|
593
|
+
});
|
|
594
|
+
ensureCleanup();
|
|
595
|
+
return id;
|
|
596
|
+
}
|
|
597
|
+
function tunnelRead(id) {
|
|
598
|
+
const entry = tunnelStore.get(id);
|
|
599
|
+
if (!entry) return null;
|
|
600
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
601
|
+
tunnelStore.delete(id);
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
entry.accessCount++;
|
|
605
|
+
if (entry.maxReads && entry.accessCount >= entry.maxReads) {
|
|
606
|
+
const value = entry.value;
|
|
607
|
+
tunnelStore.delete(id);
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
return entry.value;
|
|
611
|
+
}
|
|
612
|
+
function tunnelDestroy(id) {
|
|
613
|
+
return tunnelStore.delete(id);
|
|
614
|
+
}
|
|
615
|
+
function tunnelList() {
|
|
616
|
+
const now = Date.now();
|
|
617
|
+
const result = [];
|
|
618
|
+
for (const [id, entry] of tunnelStore) {
|
|
619
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
620
|
+
tunnelStore.delete(id);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
result.push({
|
|
624
|
+
id,
|
|
625
|
+
createdAt: entry.createdAt,
|
|
626
|
+
expiresAt: entry.expiresAt,
|
|
627
|
+
accessCount: entry.accessCount,
|
|
628
|
+
maxReads: entry.maxReads
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export {
|
|
635
|
+
checkDecay,
|
|
636
|
+
collapseEnvironment,
|
|
637
|
+
logAudit,
|
|
638
|
+
queryAudit,
|
|
639
|
+
detectAnomalies,
|
|
640
|
+
listEntanglements,
|
|
641
|
+
getSecret,
|
|
642
|
+
getEnvelope,
|
|
643
|
+
setSecret,
|
|
644
|
+
deleteSecret,
|
|
645
|
+
hasSecret,
|
|
646
|
+
listSecrets,
|
|
647
|
+
entangleSecrets,
|
|
648
|
+
tunnelCreate,
|
|
649
|
+
tunnelRead,
|
|
650
|
+
tunnelDestroy,
|
|
651
|
+
tunnelList
|
|
652
|
+
};
|
|
653
|
+
//# sourceMappingURL=chunk-3WTTWJYU.js.map
|