@i4ctime/q-ring 0.4.0 → 0.9.2
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 +341 -10
- package/dist/{chunk-IGNU622R.js → chunk-5JBU7TWN.js} +715 -124
- package/dist/chunk-5JBU7TWN.js.map +1 -0
- package/dist/chunk-WG4ZKN7Q.js +1632 -0
- package/dist/chunk-WG4ZKN7Q.js.map +1 -0
- package/dist/{dashboard-32PCZF7D.js → dashboard-JT5ZNLT5.js} +41 -16
- package/dist/dashboard-JT5ZNLT5.js.map +1 -0
- package/dist/{dashboard-HVIQO6NT.js → dashboard-Q5OQRQCX.js} +41 -16
- package/dist/dashboard-Q5OQRQCX.js.map +1 -0
- package/dist/index.js +1213 -39
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1066 -48
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6IQ5SFLI.js +0 -967
- package/dist/chunk-6IQ5SFLI.js.map +0 -1
- package/dist/chunk-IGNU622R.js.map +0 -1
- package/dist/dashboard-32PCZF7D.js.map +0 -1
- package/dist/dashboard-HVIQO6NT.js.map +0 -1
|
@@ -0,0 +1,1632 @@
|
|
|
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
|
+
rotationFormat: opts?.rotationFormat,
|
|
25
|
+
rotationPrefix: opts?.rotationPrefix,
|
|
26
|
+
provider: opts?.provider,
|
|
27
|
+
requiresApproval: opts?.requiresApproval,
|
|
28
|
+
jitProvider: opts?.jitProvider
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseEnvelope(raw) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (parsed && typeof parsed === "object" && parsed.v === 1) {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function wrapLegacy(rawValue) {
|
|
43
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
44
|
+
return {
|
|
45
|
+
v: 1,
|
|
46
|
+
value: rawValue,
|
|
47
|
+
meta: {
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
accessCount: 0
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function serializeEnvelope(envelope) {
|
|
55
|
+
return JSON.stringify(envelope);
|
|
56
|
+
}
|
|
57
|
+
function collapseValue(envelope, env) {
|
|
58
|
+
if (envelope.states) {
|
|
59
|
+
const targetEnv = env ?? envelope.defaultEnv;
|
|
60
|
+
if (targetEnv && envelope.states[targetEnv]) {
|
|
61
|
+
return envelope.states[targetEnv];
|
|
62
|
+
}
|
|
63
|
+
if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
|
|
64
|
+
return envelope.states[envelope.defaultEnv];
|
|
65
|
+
}
|
|
66
|
+
const keys = Object.keys(envelope.states);
|
|
67
|
+
if (keys.length > 0) {
|
|
68
|
+
return envelope.states[keys[0]];
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return envelope.value ?? null;
|
|
73
|
+
}
|
|
74
|
+
function checkDecay(envelope) {
|
|
75
|
+
if (!envelope.meta.expiresAt) {
|
|
76
|
+
return {
|
|
77
|
+
isExpired: false,
|
|
78
|
+
isStale: false,
|
|
79
|
+
lifetimePercent: 0,
|
|
80
|
+
secondsRemaining: null,
|
|
81
|
+
timeRemaining: null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
86
|
+
const created = new Date(envelope.meta.createdAt).getTime();
|
|
87
|
+
const totalLifetime = expires - created;
|
|
88
|
+
const elapsed = now - created;
|
|
89
|
+
const remaining = expires - now;
|
|
90
|
+
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
91
|
+
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
92
|
+
let timeRemaining = null;
|
|
93
|
+
if (remaining > 0) {
|
|
94
|
+
const days = Math.floor(remaining / 864e5);
|
|
95
|
+
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
96
|
+
const minutes = Math.floor(remaining % 36e5 / 6e4);
|
|
97
|
+
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
98
|
+
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
99
|
+
else timeRemaining = `${minutes}m`;
|
|
100
|
+
} else {
|
|
101
|
+
timeRemaining = "expired";
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
isExpired: remaining <= 0,
|
|
105
|
+
isStale: lifetimePercent >= 75,
|
|
106
|
+
lifetimePercent,
|
|
107
|
+
secondsRemaining,
|
|
108
|
+
timeRemaining
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function recordAccess(envelope) {
|
|
112
|
+
return {
|
|
113
|
+
...envelope,
|
|
114
|
+
meta: {
|
|
115
|
+
...envelope.meta,
|
|
116
|
+
accessCount: envelope.meta.accessCount + 1,
|
|
117
|
+
lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/core/collapse.ts
|
|
123
|
+
import { execSync } from "child_process";
|
|
124
|
+
import { existsSync, readFileSync } from "fs";
|
|
125
|
+
import { join } from "path";
|
|
126
|
+
var BRANCH_ENV_MAP = {
|
|
127
|
+
main: "prod",
|
|
128
|
+
master: "prod",
|
|
129
|
+
production: "prod",
|
|
130
|
+
develop: "dev",
|
|
131
|
+
development: "dev",
|
|
132
|
+
dev: "dev",
|
|
133
|
+
staging: "staging",
|
|
134
|
+
stage: "staging",
|
|
135
|
+
test: "test",
|
|
136
|
+
testing: "test"
|
|
137
|
+
};
|
|
138
|
+
function detectGitBranch(cwd) {
|
|
139
|
+
try {
|
|
140
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
141
|
+
cwd: cwd ?? process.cwd(),
|
|
142
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
timeout: 3e3
|
|
145
|
+
}).trim();
|
|
146
|
+
return branch || null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function readProjectConfig(projectPath) {
|
|
152
|
+
const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
|
|
153
|
+
try {
|
|
154
|
+
if (existsSync(configPath)) {
|
|
155
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function collapseEnvironment(ctx = {}) {
|
|
162
|
+
if (ctx.explicit) {
|
|
163
|
+
return { env: ctx.explicit, source: "explicit" };
|
|
164
|
+
}
|
|
165
|
+
const qringEnv = process.env.QRING_ENV;
|
|
166
|
+
if (qringEnv) {
|
|
167
|
+
return { env: qringEnv, source: "QRING_ENV" };
|
|
168
|
+
}
|
|
169
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
170
|
+
if (nodeEnv) {
|
|
171
|
+
const mapped = mapEnvName(nodeEnv);
|
|
172
|
+
return { env: mapped, source: "NODE_ENV" };
|
|
173
|
+
}
|
|
174
|
+
const config = readProjectConfig(ctx.projectPath);
|
|
175
|
+
if (config?.env) {
|
|
176
|
+
return { env: config.env, source: "project-config" };
|
|
177
|
+
}
|
|
178
|
+
const branch = detectGitBranch(ctx.projectPath);
|
|
179
|
+
if (branch) {
|
|
180
|
+
const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
|
|
181
|
+
const mapped = branchMap[branch] ?? matchGlob(branchMap, branch);
|
|
182
|
+
if (mapped) {
|
|
183
|
+
return { env: mapped, source: "git-branch" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (config?.defaultEnv) {
|
|
187
|
+
return { env: config.defaultEnv, source: "project-config" };
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function matchGlob(branchMap, branch) {
|
|
192
|
+
for (const [pattern, env] of Object.entries(branchMap)) {
|
|
193
|
+
if (!pattern.includes("*")) continue;
|
|
194
|
+
const regex = new RegExp(
|
|
195
|
+
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
196
|
+
);
|
|
197
|
+
if (regex.test(branch)) return env;
|
|
198
|
+
}
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function mapEnvName(raw) {
|
|
202
|
+
const lower = raw.toLowerCase();
|
|
203
|
+
if (lower === "production") return "prod";
|
|
204
|
+
if (lower === "development") return "dev";
|
|
205
|
+
return lower;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/core/observer.ts
|
|
209
|
+
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
210
|
+
import { join as join2 } from "path";
|
|
211
|
+
import { homedir } from "os";
|
|
212
|
+
import { createHash } from "crypto";
|
|
213
|
+
function getAuditDir() {
|
|
214
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
215
|
+
if (!existsSync2(dir)) {
|
|
216
|
+
mkdirSync(dir, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
return dir;
|
|
219
|
+
}
|
|
220
|
+
function getAuditPath() {
|
|
221
|
+
return join2(getAuditDir(), "audit.jsonl");
|
|
222
|
+
}
|
|
223
|
+
function getLastLineHash() {
|
|
224
|
+
const path = getAuditPath();
|
|
225
|
+
if (!existsSync2(path)) return void 0;
|
|
226
|
+
try {
|
|
227
|
+
const fd = openSync(path, "r");
|
|
228
|
+
const stat = fstatSync(fd);
|
|
229
|
+
if (stat.size === 0) {
|
|
230
|
+
closeSync(fd);
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
const tailSize = Math.min(stat.size, 8192);
|
|
234
|
+
const buf = Buffer.alloc(tailSize);
|
|
235
|
+
readSync(fd, buf, 0, tailSize, stat.size - tailSize);
|
|
236
|
+
closeSync(fd);
|
|
237
|
+
const tail = buf.toString("utf8");
|
|
238
|
+
const lines = tail.split("\n").filter((l) => l.trim());
|
|
239
|
+
if (lines.length === 0) return void 0;
|
|
240
|
+
const lastLine = lines[lines.length - 1];
|
|
241
|
+
return createHash("sha256").update(lastLine).digest("hex");
|
|
242
|
+
} catch {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function logAudit(event) {
|
|
247
|
+
const prevHash = getLastLineHash();
|
|
248
|
+
const full = {
|
|
249
|
+
...event,
|
|
250
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
251
|
+
pid: process.pid,
|
|
252
|
+
prevHash
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function queryAudit(query = {}) {
|
|
260
|
+
const path = getAuditPath();
|
|
261
|
+
if (!existsSync2(path)) return [];
|
|
262
|
+
try {
|
|
263
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
264
|
+
let events = lines.map((line) => {
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(line);
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}).filter((e) => e !== null);
|
|
271
|
+
if (query.key) events = events.filter((e) => e.key === query.key);
|
|
272
|
+
if (query.action) events = events.filter((e) => e.action === query.action);
|
|
273
|
+
if (query.source) events = events.filter((e) => e.source === query.source);
|
|
274
|
+
if (query.correlationId) events = events.filter((e) => e.correlationId === query.correlationId);
|
|
275
|
+
if (query.since) {
|
|
276
|
+
const since = new Date(query.since).getTime();
|
|
277
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
|
|
278
|
+
}
|
|
279
|
+
events.sort(
|
|
280
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
281
|
+
);
|
|
282
|
+
if (query.limit) events = events.slice(0, query.limit);
|
|
283
|
+
return events;
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function verifyAuditChain() {
|
|
289
|
+
const path = getAuditPath();
|
|
290
|
+
if (!existsSync2(path)) {
|
|
291
|
+
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
292
|
+
}
|
|
293
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
294
|
+
if (lines.length === 0) {
|
|
295
|
+
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
296
|
+
}
|
|
297
|
+
let validEvents = 0;
|
|
298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
299
|
+
let event;
|
|
300
|
+
try {
|
|
301
|
+
event = JSON.parse(lines[i]);
|
|
302
|
+
} catch {
|
|
303
|
+
return {
|
|
304
|
+
totalEvents: lines.length,
|
|
305
|
+
validEvents,
|
|
306
|
+
brokenAt: i,
|
|
307
|
+
intact: false
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (i === 0) {
|
|
311
|
+
validEvents++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const expectedHash = createHash("sha256").update(lines[i - 1]).digest("hex");
|
|
315
|
+
if (event.prevHash !== expectedHash) {
|
|
316
|
+
return {
|
|
317
|
+
totalEvents: lines.length,
|
|
318
|
+
validEvents,
|
|
319
|
+
brokenAt: i,
|
|
320
|
+
brokenEvent: event,
|
|
321
|
+
intact: false
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
validEvents++;
|
|
325
|
+
}
|
|
326
|
+
return { totalEvents: lines.length, validEvents, intact: true };
|
|
327
|
+
}
|
|
328
|
+
function exportAudit(opts = {}) {
|
|
329
|
+
const path = getAuditPath();
|
|
330
|
+
if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
|
|
331
|
+
const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
|
|
332
|
+
let events = lines.map((l) => {
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(l);
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}).filter((e) => e !== null);
|
|
339
|
+
if (opts.since) {
|
|
340
|
+
const since = new Date(opts.since).getTime();
|
|
341
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
|
|
342
|
+
}
|
|
343
|
+
if (opts.until) {
|
|
344
|
+
const until = new Date(opts.until).getTime();
|
|
345
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() <= until);
|
|
346
|
+
}
|
|
347
|
+
if (opts.format === "json") {
|
|
348
|
+
return JSON.stringify(events, null, 2);
|
|
349
|
+
}
|
|
350
|
+
if (opts.format === "csv") {
|
|
351
|
+
const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
|
|
352
|
+
const rows = events.map(
|
|
353
|
+
(e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
|
|
354
|
+
);
|
|
355
|
+
return [header, ...rows].join("\n");
|
|
356
|
+
}
|
|
357
|
+
return events.map((e) => JSON.stringify(e)).join("\n");
|
|
358
|
+
}
|
|
359
|
+
function detectAnomalies(key) {
|
|
360
|
+
const recent = queryAudit({
|
|
361
|
+
key,
|
|
362
|
+
action: "read",
|
|
363
|
+
since: new Date(Date.now() - 36e5).toISOString()
|
|
364
|
+
});
|
|
365
|
+
const anomalies = [];
|
|
366
|
+
if (key && recent.length > 50) {
|
|
367
|
+
anomalies.push({
|
|
368
|
+
type: "burst",
|
|
369
|
+
description: `${recent.length} reads of "${key}" in the last hour`,
|
|
370
|
+
events: recent.slice(0, 10)
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const nightAccess = recent.filter((e) => {
|
|
374
|
+
const hour = new Date(e.timestamp).getHours();
|
|
375
|
+
return hour >= 1 && hour < 5;
|
|
376
|
+
});
|
|
377
|
+
if (nightAccess.length > 0) {
|
|
378
|
+
anomalies.push({
|
|
379
|
+
type: "unusual-hour",
|
|
380
|
+
description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
|
|
381
|
+
events: nightAccess
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const verification = verifyAuditChain();
|
|
385
|
+
if (!verification.intact) {
|
|
386
|
+
anomalies.push({
|
|
387
|
+
type: "tampered",
|
|
388
|
+
description: `Audit chain broken at event #${verification.brokenAt}`,
|
|
389
|
+
events: verification.brokenEvent ? [verification.brokenEvent] : []
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return anomalies;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/core/entanglement.ts
|
|
396
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
397
|
+
import { join as join3 } from "path";
|
|
398
|
+
import { homedir as homedir2 } from "os";
|
|
399
|
+
function getRegistryPath() {
|
|
400
|
+
const dir = join3(homedir2(), ".config", "q-ring");
|
|
401
|
+
if (!existsSync3(dir)) {
|
|
402
|
+
mkdirSync2(dir, { recursive: true });
|
|
403
|
+
}
|
|
404
|
+
return join3(dir, "entanglement.json");
|
|
405
|
+
}
|
|
406
|
+
function loadRegistry() {
|
|
407
|
+
const path = getRegistryPath();
|
|
408
|
+
if (!existsSync3(path)) {
|
|
409
|
+
return { pairs: [] };
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
413
|
+
} catch {
|
|
414
|
+
return { pairs: [] };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function saveRegistry(registry2) {
|
|
418
|
+
writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2));
|
|
419
|
+
}
|
|
420
|
+
function entangle(source, target) {
|
|
421
|
+
const registry2 = loadRegistry();
|
|
422
|
+
const exists = registry2.pairs.some(
|
|
423
|
+
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
424
|
+
);
|
|
425
|
+
if (!exists) {
|
|
426
|
+
registry2.pairs.push({
|
|
427
|
+
source,
|
|
428
|
+
target,
|
|
429
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
430
|
+
});
|
|
431
|
+
registry2.pairs.push({
|
|
432
|
+
source: target,
|
|
433
|
+
target: source,
|
|
434
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
435
|
+
});
|
|
436
|
+
saveRegistry(registry2);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function disentangle(source, target) {
|
|
440
|
+
const registry2 = loadRegistry();
|
|
441
|
+
registry2.pairs = registry2.pairs.filter(
|
|
442
|
+
(p) => !(p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key || p.source.service === target.service && p.source.key === target.key && p.target.service === source.service && p.target.key === source.key)
|
|
443
|
+
);
|
|
444
|
+
saveRegistry(registry2);
|
|
445
|
+
}
|
|
446
|
+
function findEntangled(source) {
|
|
447
|
+
const registry2 = loadRegistry();
|
|
448
|
+
return registry2.pairs.filter(
|
|
449
|
+
(p) => p.source.service === source.service && p.source.key === source.key
|
|
450
|
+
).map((p) => p.target);
|
|
451
|
+
}
|
|
452
|
+
function listEntanglements() {
|
|
453
|
+
return loadRegistry().pairs;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/core/keyring.ts
|
|
457
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
458
|
+
|
|
459
|
+
// src/utils/hash.ts
|
|
460
|
+
import { createHash as createHash2 } from "crypto";
|
|
461
|
+
function hashProjectPath(projectPath) {
|
|
462
|
+
return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/core/scope.ts
|
|
466
|
+
var SERVICE_PREFIX = "q-ring";
|
|
467
|
+
function globalService() {
|
|
468
|
+
return `${SERVICE_PREFIX}:global`;
|
|
469
|
+
}
|
|
470
|
+
function projectService(projectPath) {
|
|
471
|
+
const hash = hashProjectPath(projectPath);
|
|
472
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
473
|
+
}
|
|
474
|
+
function teamService(teamId) {
|
|
475
|
+
return `${SERVICE_PREFIX}:team:${teamId}`;
|
|
476
|
+
}
|
|
477
|
+
function orgService(orgId) {
|
|
478
|
+
return `${SERVICE_PREFIX}:org:${orgId}`;
|
|
479
|
+
}
|
|
480
|
+
function resolveScope(opts) {
|
|
481
|
+
const { scope, projectPath, teamId, orgId } = opts;
|
|
482
|
+
if (scope === "global") {
|
|
483
|
+
return [{ scope: "global", service: globalService() }];
|
|
484
|
+
}
|
|
485
|
+
if (scope === "project") {
|
|
486
|
+
if (!projectPath) throw new Error("Project path is required for project scope");
|
|
487
|
+
return [{ scope: "project", service: projectService(projectPath), projectPath }];
|
|
488
|
+
}
|
|
489
|
+
if (scope === "team") {
|
|
490
|
+
if (!teamId) throw new Error("Team ID is required for team scope");
|
|
491
|
+
return [{ scope: "team", service: teamService(teamId), teamId }];
|
|
492
|
+
}
|
|
493
|
+
if (scope === "org") {
|
|
494
|
+
if (!orgId) throw new Error("Org ID is required for org scope");
|
|
495
|
+
return [{ scope: "org", service: orgService(orgId), orgId }];
|
|
496
|
+
}
|
|
497
|
+
const chain = [];
|
|
498
|
+
if (projectPath) {
|
|
499
|
+
chain.push({ scope: "project", service: projectService(projectPath), projectPath });
|
|
500
|
+
}
|
|
501
|
+
if (teamId) {
|
|
502
|
+
chain.push({ scope: "team", service: teamService(teamId), teamId });
|
|
503
|
+
}
|
|
504
|
+
if (orgId) {
|
|
505
|
+
chain.push({ scope: "org", service: orgService(orgId), orgId });
|
|
506
|
+
}
|
|
507
|
+
chain.push({ scope: "global", service: globalService() });
|
|
508
|
+
return chain;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/core/hooks.ts
|
|
512
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
513
|
+
import { join as join4 } from "path";
|
|
514
|
+
import { homedir as homedir3 } from "os";
|
|
515
|
+
import { exec } from "child_process";
|
|
516
|
+
import { randomUUID } from "crypto";
|
|
517
|
+
import { lookup } from "dns/promises";
|
|
518
|
+
|
|
519
|
+
// src/utils/http-request.ts
|
|
520
|
+
import { request as httpsRequest } from "https";
|
|
521
|
+
import { request as httpRequest } from "http";
|
|
522
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
523
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
524
|
+
function httpRequest_(opts) {
|
|
525
|
+
const {
|
|
526
|
+
url,
|
|
527
|
+
method = "GET",
|
|
528
|
+
headers = {},
|
|
529
|
+
body,
|
|
530
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
531
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
532
|
+
} = opts;
|
|
533
|
+
return new Promise((resolve, reject) => {
|
|
534
|
+
const parsed = new URL(url);
|
|
535
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
536
|
+
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequest;
|
|
540
|
+
const reqHeaders = { ...headers };
|
|
541
|
+
if (body && !reqHeaders["Content-Length"]) {
|
|
542
|
+
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
543
|
+
}
|
|
544
|
+
const req = reqFn(
|
|
545
|
+
url,
|
|
546
|
+
{ method, headers: reqHeaders, timeout: timeoutMs },
|
|
547
|
+
(res) => {
|
|
548
|
+
const chunks = [];
|
|
549
|
+
let totalBytes = 0;
|
|
550
|
+
let truncated = false;
|
|
551
|
+
res.on("data", (chunk) => {
|
|
552
|
+
totalBytes += chunk.length;
|
|
553
|
+
if (totalBytes > maxResponseBytes) {
|
|
554
|
+
truncated = true;
|
|
555
|
+
res.destroy();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
chunks.push(chunk);
|
|
559
|
+
});
|
|
560
|
+
let settled = false;
|
|
561
|
+
const settle = (result) => {
|
|
562
|
+
if (!settled) {
|
|
563
|
+
settled = true;
|
|
564
|
+
resolve(result);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const fail = (err) => {
|
|
568
|
+
if (!settled) {
|
|
569
|
+
settled = true;
|
|
570
|
+
reject(err);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
574
|
+
res.on("end", () => {
|
|
575
|
+
settle({
|
|
576
|
+
statusCode: res.statusCode ?? 0,
|
|
577
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
578
|
+
truncated
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
res.on("close", () => {
|
|
582
|
+
settle({
|
|
583
|
+
statusCode: res.statusCode ?? 0,
|
|
584
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
585
|
+
truncated
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
591
|
+
req.on("timeout", () => {
|
|
592
|
+
req.destroy();
|
|
593
|
+
reject(new Error("Request timed out"));
|
|
594
|
+
});
|
|
595
|
+
if (body) req.write(body);
|
|
596
|
+
req.end();
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/core/hooks.ts
|
|
601
|
+
function getRegistryPath2() {
|
|
602
|
+
const dir = join4(homedir3(), ".config", "q-ring");
|
|
603
|
+
if (!existsSync4(dir)) {
|
|
604
|
+
mkdirSync3(dir, { recursive: true });
|
|
605
|
+
}
|
|
606
|
+
return join4(dir, "hooks.json");
|
|
607
|
+
}
|
|
608
|
+
function loadRegistry2() {
|
|
609
|
+
const path = getRegistryPath2();
|
|
610
|
+
if (!existsSync4(path)) {
|
|
611
|
+
return { hooks: [] };
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
615
|
+
} catch {
|
|
616
|
+
return { hooks: [] };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function saveRegistry2(registry2) {
|
|
620
|
+
writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2));
|
|
621
|
+
}
|
|
622
|
+
function registerHook(entry) {
|
|
623
|
+
const registry2 = loadRegistry2();
|
|
624
|
+
const hook = {
|
|
625
|
+
...entry,
|
|
626
|
+
id: randomUUID().slice(0, 8),
|
|
627
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
628
|
+
};
|
|
629
|
+
registry2.hooks.push(hook);
|
|
630
|
+
saveRegistry2(registry2);
|
|
631
|
+
return hook;
|
|
632
|
+
}
|
|
633
|
+
function removeHook(id) {
|
|
634
|
+
const registry2 = loadRegistry2();
|
|
635
|
+
const before = registry2.hooks.length;
|
|
636
|
+
registry2.hooks = registry2.hooks.filter((h) => h.id !== id);
|
|
637
|
+
if (registry2.hooks.length < before) {
|
|
638
|
+
saveRegistry2(registry2);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
function listHooks() {
|
|
644
|
+
return loadRegistry2().hooks;
|
|
645
|
+
}
|
|
646
|
+
function enableHook(id) {
|
|
647
|
+
const registry2 = loadRegistry2();
|
|
648
|
+
const hook = registry2.hooks.find((h) => h.id === id);
|
|
649
|
+
if (!hook) return false;
|
|
650
|
+
hook.enabled = true;
|
|
651
|
+
saveRegistry2(registry2);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
function disableHook(id) {
|
|
655
|
+
const registry2 = loadRegistry2();
|
|
656
|
+
const hook = registry2.hooks.find((h) => h.id === id);
|
|
657
|
+
if (!hook) return false;
|
|
658
|
+
hook.enabled = false;
|
|
659
|
+
saveRegistry2(registry2);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
function matchesHook(hook, payload, tags) {
|
|
663
|
+
if (!hook.enabled) return false;
|
|
664
|
+
const m = hook.match;
|
|
665
|
+
if (m.action?.length && !m.action.includes(payload.action)) return false;
|
|
666
|
+
if (m.key && m.key !== payload.key) return false;
|
|
667
|
+
if (m.keyPattern) {
|
|
668
|
+
const regex = new RegExp(
|
|
669
|
+
"^" + m.keyPattern.replace(/\*/g, ".*") + "$",
|
|
670
|
+
"i"
|
|
671
|
+
);
|
|
672
|
+
if (!regex.test(payload.key)) return false;
|
|
673
|
+
}
|
|
674
|
+
if (m.tag && (!tags || !tags.includes(m.tag))) return false;
|
|
675
|
+
if (m.scope && m.scope !== payload.scope) return false;
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
function executeShell(command, payload) {
|
|
679
|
+
return new Promise((resolve) => {
|
|
680
|
+
const env = {
|
|
681
|
+
...process.env,
|
|
682
|
+
QRING_HOOK_KEY: payload.key,
|
|
683
|
+
QRING_HOOK_ACTION: payload.action,
|
|
684
|
+
QRING_HOOK_SCOPE: payload.scope
|
|
685
|
+
};
|
|
686
|
+
exec(command, { timeout: 3e4, env }, (err, stdout, stderr) => {
|
|
687
|
+
if (err) {
|
|
688
|
+
resolve({ hookId: "", success: false, message: `Shell error: ${err.message}` });
|
|
689
|
+
} else {
|
|
690
|
+
resolve({ hookId: "", success: true, message: stdout.trim() || "OK" });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function isPrivateIP(ip) {
|
|
696
|
+
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
697
|
+
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
698
|
+
const ipv4Mapped = ip.match(ipv4Re);
|
|
699
|
+
if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
|
|
700
|
+
if (/^127\./.test(ip)) return true;
|
|
701
|
+
if (/^10\./.test(ip)) return true;
|
|
702
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
703
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
704
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
705
|
+
if (ip === "0.0.0.0") return true;
|
|
706
|
+
if (ip === "::1" || ip === "::") return true;
|
|
707
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
|
|
708
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
async function checkSSRF(url) {
|
|
712
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
713
|
+
try {
|
|
714
|
+
const parsed = new URL(url);
|
|
715
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
716
|
+
if (isPrivateIP(hostname)) {
|
|
717
|
+
return `Blocked: hook URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
718
|
+
}
|
|
719
|
+
const results = await lookup(hostname, { all: true });
|
|
720
|
+
for (const { address } of results) {
|
|
721
|
+
if (isPrivateIP(address)) {
|
|
722
|
+
return `Blocked: hook URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
async function executeHttp(url, payload) {
|
|
730
|
+
const ssrfBlock = await checkSSRF(url);
|
|
731
|
+
if (ssrfBlock) {
|
|
732
|
+
logAudit({
|
|
733
|
+
action: "policy_deny",
|
|
734
|
+
key: payload.key,
|
|
735
|
+
scope: payload.scope,
|
|
736
|
+
source: payload.source,
|
|
737
|
+
detail: `hook SSRF blocked: ${url}`
|
|
738
|
+
});
|
|
739
|
+
return { hookId: "", success: false, message: ssrfBlock };
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const body = JSON.stringify(payload);
|
|
743
|
+
const res = await httpRequest_({
|
|
744
|
+
url,
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers: {
|
|
747
|
+
"Content-Type": "application/json",
|
|
748
|
+
"User-Agent": "q-ring-hooks/1.0"
|
|
749
|
+
},
|
|
750
|
+
body,
|
|
751
|
+
timeoutMs: 1e4
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
hookId: "",
|
|
755
|
+
success: res.statusCode >= 200 && res.statusCode < 300,
|
|
756
|
+
message: `HTTP ${res.statusCode}`
|
|
757
|
+
};
|
|
758
|
+
} catch (err) {
|
|
759
|
+
return {
|
|
760
|
+
hookId: "",
|
|
761
|
+
success: false,
|
|
762
|
+
message: err instanceof Error ? err.message : "HTTP error"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function executeSignal(target, signal = "SIGHUP") {
|
|
767
|
+
return new Promise((resolve) => {
|
|
768
|
+
const pid = parseInt(target, 10);
|
|
769
|
+
if (!isNaN(pid)) {
|
|
770
|
+
try {
|
|
771
|
+
process.kill(pid, signal);
|
|
772
|
+
resolve({ hookId: "", success: true, message: `Signal ${signal} sent to PID ${pid}` });
|
|
773
|
+
} catch (err) {
|
|
774
|
+
resolve({ hookId: "", success: false, message: `Signal error: ${err instanceof Error ? err.message : String(err)}` });
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
exec(`pgrep -f "${target}"`, { timeout: 5e3 }, (err, stdout) => {
|
|
779
|
+
if (err || !stdout.trim()) {
|
|
780
|
+
resolve({ hookId: "", success: false, message: `Process "${target}" not found` });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
784
|
+
let sent = 0;
|
|
785
|
+
for (const p of pids) {
|
|
786
|
+
try {
|
|
787
|
+
process.kill(p, signal);
|
|
788
|
+
sent++;
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
resolve({ hookId: "", success: sent > 0, message: `Signal ${signal} sent to ${sent} process(es)` });
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
async function executeHook(hook, payload) {
|
|
797
|
+
let result;
|
|
798
|
+
switch (hook.type) {
|
|
799
|
+
case "shell":
|
|
800
|
+
result = hook.command ? await executeShell(hook.command, payload) : { hookId: hook.id, success: false, message: "No command specified" };
|
|
801
|
+
break;
|
|
802
|
+
case "http":
|
|
803
|
+
result = hook.url ? await executeHttp(hook.url, payload) : { hookId: hook.id, success: false, message: "No URL specified" };
|
|
804
|
+
break;
|
|
805
|
+
case "signal":
|
|
806
|
+
result = hook.signal ? await executeSignal(hook.signal.target, hook.signal.signal) : { hookId: hook.id, success: false, message: "No signal target specified" };
|
|
807
|
+
break;
|
|
808
|
+
default:
|
|
809
|
+
result = { hookId: hook.id, success: false, message: `Unknown hook type: ${hook.type}` };
|
|
810
|
+
}
|
|
811
|
+
result.hookId = hook.id;
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
async function fireHooks(payload, tags) {
|
|
815
|
+
const hooks = listHooks();
|
|
816
|
+
const matching = hooks.filter((h) => matchesHook(h, payload, tags));
|
|
817
|
+
if (matching.length === 0) return [];
|
|
818
|
+
const results = await Promise.allSettled(
|
|
819
|
+
matching.map((h) => executeHook(h, payload))
|
|
820
|
+
);
|
|
821
|
+
const hookResults = [];
|
|
822
|
+
for (const r of results) {
|
|
823
|
+
if (r.status === "fulfilled") {
|
|
824
|
+
hookResults.push(r.value);
|
|
825
|
+
} else {
|
|
826
|
+
hookResults.push({
|
|
827
|
+
hookId: "unknown",
|
|
828
|
+
success: false,
|
|
829
|
+
message: r.reason?.message ?? "Hook execution failed"
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const r of hookResults) {
|
|
834
|
+
try {
|
|
835
|
+
logAudit({
|
|
836
|
+
action: "write",
|
|
837
|
+
key: payload.key,
|
|
838
|
+
scope: payload.scope,
|
|
839
|
+
source: payload.source,
|
|
840
|
+
detail: `hook:${r.hookId} ${r.success ? "ok" : "fail"} \u2014 ${r.message}`
|
|
841
|
+
});
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return hookResults;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/core/approval.ts
|
|
849
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
850
|
+
import { join as join5 } from "path";
|
|
851
|
+
import { homedir as homedir4 } from "os";
|
|
852
|
+
import { createHmac, randomBytes } from "crypto";
|
|
853
|
+
function getHmacSecret() {
|
|
854
|
+
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
855
|
+
const dir = join5(homedir4(), ".config", "q-ring");
|
|
856
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
|
|
857
|
+
if (existsSync5(secretPath)) {
|
|
858
|
+
return readFileSync5(secretPath, "utf8").trim();
|
|
859
|
+
}
|
|
860
|
+
const secret = randomBytes(32).toString("hex");
|
|
861
|
+
writeFileSync4(secretPath, secret, { mode: 384 });
|
|
862
|
+
return secret;
|
|
863
|
+
}
|
|
864
|
+
function computeHmac(entry) {
|
|
865
|
+
const payload = `${entry.id}|${entry.key}|${entry.scope}|${entry.reason}|${entry.grantedBy}|${entry.grantedAt}|${entry.expiresAt}`;
|
|
866
|
+
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
867
|
+
}
|
|
868
|
+
function verifyHmac(entry) {
|
|
869
|
+
const expected = computeHmac(entry);
|
|
870
|
+
return expected === entry.hmac;
|
|
871
|
+
}
|
|
872
|
+
function getRegistryPath3() {
|
|
873
|
+
const dir = join5(homedir4(), ".config", "q-ring");
|
|
874
|
+
if (!existsSync5(dir)) {
|
|
875
|
+
mkdirSync4(dir, { recursive: true });
|
|
876
|
+
}
|
|
877
|
+
return join5(dir, "approvals.json");
|
|
878
|
+
}
|
|
879
|
+
function loadRegistry3() {
|
|
880
|
+
const path = getRegistryPath3();
|
|
881
|
+
if (!existsSync5(path)) {
|
|
882
|
+
return { approvals: [] };
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
886
|
+
} catch {
|
|
887
|
+
return { approvals: [] };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function saveRegistry3(registry2) {
|
|
891
|
+
writeFileSync4(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
|
|
892
|
+
}
|
|
893
|
+
function cleanup(registry2) {
|
|
894
|
+
const now = Date.now();
|
|
895
|
+
registry2.approvals = registry2.approvals.filter(
|
|
896
|
+
(a) => new Date(a.expiresAt).getTime() > now
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
function grantApproval(key, scope, ttlSeconds = 3600, grantOpts = {}) {
|
|
900
|
+
const registry2 = loadRegistry3();
|
|
901
|
+
cleanup(registry2);
|
|
902
|
+
const id = randomBytes(8).toString("hex");
|
|
903
|
+
const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
904
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
905
|
+
const partial = {
|
|
906
|
+
id,
|
|
907
|
+
key,
|
|
908
|
+
scope,
|
|
909
|
+
reason: grantOpts.reason ?? "no reason provided",
|
|
910
|
+
grantedBy: grantOpts.grantedBy ?? "cli-user",
|
|
911
|
+
grantedAt,
|
|
912
|
+
expiresAt,
|
|
913
|
+
workspace: grantOpts.workspace,
|
|
914
|
+
sessionId: grantOpts.sessionId
|
|
915
|
+
};
|
|
916
|
+
const entry = { ...partial, hmac: computeHmac(partial) };
|
|
917
|
+
const existingIdx = registry2.approvals.findIndex(
|
|
918
|
+
(a) => a.key === key && a.scope === scope
|
|
919
|
+
);
|
|
920
|
+
if (existingIdx >= 0) {
|
|
921
|
+
registry2.approvals[existingIdx] = entry;
|
|
922
|
+
} else {
|
|
923
|
+
registry2.approvals.push(entry);
|
|
924
|
+
}
|
|
925
|
+
saveRegistry3(registry2);
|
|
926
|
+
return entry;
|
|
927
|
+
}
|
|
928
|
+
function revokeApproval(key, scope) {
|
|
929
|
+
const registry2 = loadRegistry3();
|
|
930
|
+
const before = registry2.approvals.length;
|
|
931
|
+
registry2.approvals = registry2.approvals.filter(
|
|
932
|
+
(a) => !(a.key === key && a.scope === scope)
|
|
933
|
+
);
|
|
934
|
+
saveRegistry3(registry2);
|
|
935
|
+
return registry2.approvals.length < before;
|
|
936
|
+
}
|
|
937
|
+
function hasApproval(key, scope) {
|
|
938
|
+
const registry2 = loadRegistry3();
|
|
939
|
+
const entry = registry2.approvals.find(
|
|
940
|
+
(a) => a.key === key && a.scope === scope
|
|
941
|
+
);
|
|
942
|
+
if (!entry) return false;
|
|
943
|
+
if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
|
|
944
|
+
if (!verifyHmac(entry)) return false;
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
function listApprovals() {
|
|
948
|
+
const registry2 = loadRegistry3();
|
|
949
|
+
const now = Date.now();
|
|
950
|
+
return registry2.approvals.map((a) => ({
|
|
951
|
+
...a,
|
|
952
|
+
valid: new Date(a.expiresAt).getTime() > now,
|
|
953
|
+
tampered: !verifyHmac(a)
|
|
954
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/core/provision.ts
|
|
958
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
959
|
+
var ProvisionRegistry = class {
|
|
960
|
+
providers = /* @__PURE__ */ new Map();
|
|
961
|
+
register(provider) {
|
|
962
|
+
this.providers.set(provider.name, provider);
|
|
963
|
+
}
|
|
964
|
+
get(name) {
|
|
965
|
+
return this.providers.get(name);
|
|
966
|
+
}
|
|
967
|
+
listProviders() {
|
|
968
|
+
return [...this.providers.values()];
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
var awsStsProvider = {
|
|
972
|
+
name: "aws-sts",
|
|
973
|
+
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
974
|
+
provision(configRaw) {
|
|
975
|
+
let config;
|
|
976
|
+
try {
|
|
977
|
+
config = JSON.parse(configRaw);
|
|
978
|
+
} catch {
|
|
979
|
+
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
980
|
+
}
|
|
981
|
+
const roleArn = config.roleArn;
|
|
982
|
+
const sessionName = config.sessionName || "q-ring-agent";
|
|
983
|
+
const duration = config.durationSeconds || 3600;
|
|
984
|
+
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
985
|
+
try {
|
|
986
|
+
const output = execFileSync("aws", [
|
|
987
|
+
"sts",
|
|
988
|
+
"assume-role",
|
|
989
|
+
"--role-arn",
|
|
990
|
+
roleArn,
|
|
991
|
+
"--role-session-name",
|
|
992
|
+
sessionName,
|
|
993
|
+
"--duration-seconds",
|
|
994
|
+
String(duration),
|
|
995
|
+
"--output",
|
|
996
|
+
"json"
|
|
997
|
+
], { encoding: "utf8" });
|
|
998
|
+
const parsed = JSON.parse(output);
|
|
999
|
+
const creds = parsed.Credentials;
|
|
1000
|
+
const value = JSON.stringify({
|
|
1001
|
+
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
1002
|
+
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
1003
|
+
AWS_SESSION_TOKEN: creds.SessionToken
|
|
1004
|
+
});
|
|
1005
|
+
return {
|
|
1006
|
+
value,
|
|
1007
|
+
expiresAt: creds.Expiration
|
|
1008
|
+
};
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
var httpProvider = {
|
|
1015
|
+
name: "http",
|
|
1016
|
+
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
1017
|
+
provision(configRaw) {
|
|
1018
|
+
let config;
|
|
1019
|
+
try {
|
|
1020
|
+
config = JSON.parse(configRaw);
|
|
1021
|
+
} catch {
|
|
1022
|
+
throw new Error("http provider requires valid JSON config");
|
|
1023
|
+
}
|
|
1024
|
+
const url = config.url;
|
|
1025
|
+
const method = config.method || "POST";
|
|
1026
|
+
const valuePath = config.valuePath || "token";
|
|
1027
|
+
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
1028
|
+
if (!url) throw new Error("http provider requires url in config");
|
|
1029
|
+
const headers = {
|
|
1030
|
+
"User-Agent": "q-ring-jit/1.0",
|
|
1031
|
+
...config.headers ?? {}
|
|
1032
|
+
};
|
|
1033
|
+
let bodyStr;
|
|
1034
|
+
if (config.body) {
|
|
1035
|
+
headers["Content-Type"] = "application/json";
|
|
1036
|
+
bodyStr = JSON.stringify(config.body);
|
|
1037
|
+
}
|
|
1038
|
+
const scriptConfig = JSON.stringify({ url, method, headers, bodyStr });
|
|
1039
|
+
const script = `
|
|
1040
|
+
const cfg = JSON.parse(process.env.__QRING_HTTP_CFG);
|
|
1041
|
+
const parsedUrl = new URL(cfg.url);
|
|
1042
|
+
const http = require(parsedUrl.protocol === "https:" ? "node:https" : "node:http");
|
|
1043
|
+
const req = http.request(cfg.url, { method: cfg.method, headers: cfg.headers, timeout: 30000 }, (res) => {
|
|
1044
|
+
let body = "";
|
|
1045
|
+
res.on("data", (chunk) => body += chunk);
|
|
1046
|
+
res.on("end", () => process.stdout.write(body));
|
|
1047
|
+
});
|
|
1048
|
+
req.on("error", (e) => { process.stderr.write(e.message); process.exit(1); });
|
|
1049
|
+
if (cfg.bodyStr) req.write(cfg.bodyStr);
|
|
1050
|
+
req.end();
|
|
1051
|
+
`;
|
|
1052
|
+
try {
|
|
1053
|
+
const result = spawnSync("node", ["-e", script], {
|
|
1054
|
+
encoding: "utf8",
|
|
1055
|
+
timeout: 35e3,
|
|
1056
|
+
env: { ...process.env, __QRING_HTTP_CFG: scriptConfig }
|
|
1057
|
+
});
|
|
1058
|
+
if (result.status !== 0) {
|
|
1059
|
+
throw new Error(result.stderr || "HTTP request failed");
|
|
1060
|
+
}
|
|
1061
|
+
const parsed = JSON.parse(result.stdout);
|
|
1062
|
+
let val = parsed;
|
|
1063
|
+
for (const key of valuePath.split(".")) {
|
|
1064
|
+
val = val[key];
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
value: String(val),
|
|
1068
|
+
expiresAt: new Date(Date.now() + expiresInSeconds * 1e3).toISOString()
|
|
1069
|
+
};
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
var registry = new ProvisionRegistry();
|
|
1076
|
+
registry.register(awsStsProvider);
|
|
1077
|
+
registry.register(httpProvider);
|
|
1078
|
+
|
|
1079
|
+
// src/core/policy.ts
|
|
1080
|
+
var cachedPolicy = null;
|
|
1081
|
+
function loadPolicy(projectPath) {
|
|
1082
|
+
const pp = projectPath ?? process.cwd();
|
|
1083
|
+
if (cachedPolicy && cachedPolicy.path === pp) {
|
|
1084
|
+
return cachedPolicy.policy;
|
|
1085
|
+
}
|
|
1086
|
+
const config = readProjectConfig(pp);
|
|
1087
|
+
const policy = config?.policy ?? {};
|
|
1088
|
+
cachedPolicy = { path: pp, policy };
|
|
1089
|
+
return policy;
|
|
1090
|
+
}
|
|
1091
|
+
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1092
|
+
const policy = loadPolicy(projectPath);
|
|
1093
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1094
|
+
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1095
|
+
return {
|
|
1096
|
+
allowed: false,
|
|
1097
|
+
reason: `Key "${key}" is denied by project policy`,
|
|
1098
|
+
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1102
|
+
return {
|
|
1103
|
+
allowed: false,
|
|
1104
|
+
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1105
|
+
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (tags && policy.mcp.deniedTags) {
|
|
1109
|
+
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1110
|
+
if (blocked) {
|
|
1111
|
+
return {
|
|
1112
|
+
allowed: false,
|
|
1113
|
+
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1114
|
+
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1119
|
+
}
|
|
1120
|
+
function checkExecPolicy(command, projectPath) {
|
|
1121
|
+
const policy = loadPolicy(projectPath);
|
|
1122
|
+
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1123
|
+
if (policy.exec.denyCommands) {
|
|
1124
|
+
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1125
|
+
if (denied) {
|
|
1126
|
+
return {
|
|
1127
|
+
allowed: false,
|
|
1128
|
+
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1129
|
+
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (policy.exec.allowCommands) {
|
|
1134
|
+
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1135
|
+
if (!allowed) {
|
|
1136
|
+
return {
|
|
1137
|
+
allowed: false,
|
|
1138
|
+
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1139
|
+
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1144
|
+
}
|
|
1145
|
+
function getExecMaxRuntime(projectPath) {
|
|
1146
|
+
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1147
|
+
}
|
|
1148
|
+
function getPolicySummary(projectPath) {
|
|
1149
|
+
const policy = loadPolicy(projectPath);
|
|
1150
|
+
return {
|
|
1151
|
+
hasMcpPolicy: !!policy.mcp,
|
|
1152
|
+
hasExecPolicy: !!policy.exec,
|
|
1153
|
+
hasSecretPolicy: !!policy.secrets,
|
|
1154
|
+
details: policy
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/core/keyring.ts
|
|
1159
|
+
function readEnvelope(service, key) {
|
|
1160
|
+
const entry = new Entry(service, key);
|
|
1161
|
+
const raw = entry.getPassword();
|
|
1162
|
+
if (raw === null) return null;
|
|
1163
|
+
const envelope = parseEnvelope(raw);
|
|
1164
|
+
return envelope ?? wrapLegacy(raw);
|
|
1165
|
+
}
|
|
1166
|
+
function writeEnvelope(service, key, envelope) {
|
|
1167
|
+
const entry = new Entry(service, key);
|
|
1168
|
+
entry.setPassword(serializeEnvelope(envelope));
|
|
1169
|
+
}
|
|
1170
|
+
function resolveEnv(opts) {
|
|
1171
|
+
if (opts.env) return opts.env;
|
|
1172
|
+
const result = collapseEnvironment({ projectPath: opts.projectPath });
|
|
1173
|
+
return result?.env;
|
|
1174
|
+
}
|
|
1175
|
+
function resolveTemplates(value, opts, seen) {
|
|
1176
|
+
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1177
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
|
|
1178
|
+
const refKey = refKeyRaw.trim();
|
|
1179
|
+
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1180
|
+
if (refValue === null) {
|
|
1181
|
+
throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
|
|
1182
|
+
}
|
|
1183
|
+
return refValue;
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1187
|
+
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1188
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
|
|
1189
|
+
const refKey = refKeyRaw.trim();
|
|
1190
|
+
if (seen.has(refKey)) {
|
|
1191
|
+
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
1192
|
+
}
|
|
1193
|
+
const rawRef = rawValues.get(refKey);
|
|
1194
|
+
if (rawRef === void 0) {
|
|
1195
|
+
throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
|
|
1196
|
+
}
|
|
1197
|
+
const nextSeen = new Set(seen);
|
|
1198
|
+
nextSeen.add(refKey);
|
|
1199
|
+
return resolveTemplatesOffline(rawRef, rawValues, nextSeen);
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function getSecret(key, opts = {}) {
|
|
1203
|
+
const scopes = resolveScope(opts);
|
|
1204
|
+
const env = resolveEnv(opts);
|
|
1205
|
+
const source = opts.source ?? "cli";
|
|
1206
|
+
const seen = opts._seen ?? /* @__PURE__ */ new Set();
|
|
1207
|
+
if (source === "mcp") {
|
|
1208
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1209
|
+
if (!policyDecision.allowed) {
|
|
1210
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if (seen.has(key)) {
|
|
1214
|
+
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${key}`);
|
|
1215
|
+
}
|
|
1216
|
+
const nextSeen = new Set(seen);
|
|
1217
|
+
nextSeen.add(key);
|
|
1218
|
+
for (const { service, scope } of scopes) {
|
|
1219
|
+
const envelope = readEnvelope(service, key);
|
|
1220
|
+
if (!envelope) continue;
|
|
1221
|
+
const decay = checkDecay(envelope);
|
|
1222
|
+
if (decay.isExpired) {
|
|
1223
|
+
if (!opts.silent) {
|
|
1224
|
+
logAudit({
|
|
1225
|
+
action: "read",
|
|
1226
|
+
key,
|
|
1227
|
+
scope,
|
|
1228
|
+
source,
|
|
1229
|
+
detail: "blocked: secret expired (quantum decay)"
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (envelope.meta.requiresApproval && source === "mcp") {
|
|
1235
|
+
if (!hasApproval(key, scope)) {
|
|
1236
|
+
if (!opts.silent) {
|
|
1237
|
+
logAudit({
|
|
1238
|
+
action: "read",
|
|
1239
|
+
key,
|
|
1240
|
+
scope,
|
|
1241
|
+
source,
|
|
1242
|
+
detail: "blocked: requires user approval"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
let value = collapseValue(envelope, env);
|
|
1249
|
+
if (value === null) continue;
|
|
1250
|
+
if (envelope.meta.jitProvider) {
|
|
1251
|
+
const provider = registry.get(envelope.meta.jitProvider);
|
|
1252
|
+
if (provider) {
|
|
1253
|
+
let isExpired = true;
|
|
1254
|
+
if (envelope.states && envelope.states["jit"] && envelope.meta.jitExpiresAt) {
|
|
1255
|
+
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1256
|
+
}
|
|
1257
|
+
if (isExpired) {
|
|
1258
|
+
const result = provider.provision(value);
|
|
1259
|
+
envelope.states = envelope.states ?? {};
|
|
1260
|
+
envelope.states["jit"] = result.value;
|
|
1261
|
+
envelope.meta.jitExpiresAt = result.expiresAt;
|
|
1262
|
+
writeEnvelope(service, key, envelope);
|
|
1263
|
+
}
|
|
1264
|
+
value = envelope.states["jit"];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
|
|
1268
|
+
if (!opts.silent) {
|
|
1269
|
+
const updated = recordAccess(envelope);
|
|
1270
|
+
writeEnvelope(service, key, updated);
|
|
1271
|
+
logAudit({ action: "read", key, scope, env, source });
|
|
1272
|
+
}
|
|
1273
|
+
return value;
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
function getEnvelope(key, opts = {}) {
|
|
1278
|
+
const scopes = resolveScope(opts);
|
|
1279
|
+
for (const { service, scope } of scopes) {
|
|
1280
|
+
const envelope = readEnvelope(service, key);
|
|
1281
|
+
if (envelope) return { envelope, scope };
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
function setSecret(key, value, opts = {}) {
|
|
1286
|
+
const scope = opts.scope ?? "global";
|
|
1287
|
+
const scopes = resolveScope({ ...opts, scope });
|
|
1288
|
+
const { service } = scopes[0];
|
|
1289
|
+
const source = opts.source ?? "cli";
|
|
1290
|
+
const existing = readEnvelope(service, key);
|
|
1291
|
+
let envelope;
|
|
1292
|
+
const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
|
|
1293
|
+
const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
|
|
1294
|
+
const prov = opts.provider ?? existing?.meta.provider;
|
|
1295
|
+
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1296
|
+
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1297
|
+
if (opts.states) {
|
|
1298
|
+
envelope = createEnvelope("", {
|
|
1299
|
+
states: opts.states,
|
|
1300
|
+
defaultEnv: opts.defaultEnv,
|
|
1301
|
+
description: opts.description,
|
|
1302
|
+
tags: opts.tags,
|
|
1303
|
+
ttlSeconds: opts.ttlSeconds,
|
|
1304
|
+
expiresAt: opts.expiresAt,
|
|
1305
|
+
entangled: existing?.meta.entangled,
|
|
1306
|
+
rotationFormat: rotFmt,
|
|
1307
|
+
rotationPrefix: rotPfx,
|
|
1308
|
+
provider: prov,
|
|
1309
|
+
requiresApproval: reqApp,
|
|
1310
|
+
jitProvider: jitProv
|
|
1311
|
+
});
|
|
1312
|
+
} else {
|
|
1313
|
+
envelope = createEnvelope(value, {
|
|
1314
|
+
description: opts.description,
|
|
1315
|
+
tags: opts.tags,
|
|
1316
|
+
ttlSeconds: opts.ttlSeconds,
|
|
1317
|
+
expiresAt: opts.expiresAt,
|
|
1318
|
+
entangled: existing?.meta.entangled,
|
|
1319
|
+
rotationFormat: rotFmt,
|
|
1320
|
+
rotationPrefix: rotPfx,
|
|
1321
|
+
provider: prov,
|
|
1322
|
+
requiresApproval: reqApp,
|
|
1323
|
+
jitProvider: jitProv
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
if (existing) {
|
|
1327
|
+
envelope.meta.createdAt = existing.meta.createdAt;
|
|
1328
|
+
envelope.meta.accessCount = existing.meta.accessCount;
|
|
1329
|
+
}
|
|
1330
|
+
writeEnvelope(service, key, envelope);
|
|
1331
|
+
logAudit({ action: "write", key, scope, source });
|
|
1332
|
+
const entangled = findEntangled({ service, key });
|
|
1333
|
+
for (const target of entangled) {
|
|
1334
|
+
try {
|
|
1335
|
+
const targetEnvelope = readEnvelope(target.service, target.key);
|
|
1336
|
+
if (targetEnvelope) {
|
|
1337
|
+
if (opts.states) {
|
|
1338
|
+
targetEnvelope.states = opts.states;
|
|
1339
|
+
} else {
|
|
1340
|
+
targetEnvelope.value = value;
|
|
1341
|
+
}
|
|
1342
|
+
targetEnvelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1343
|
+
writeEnvelope(target.service, target.key, targetEnvelope);
|
|
1344
|
+
logAudit({
|
|
1345
|
+
action: "entangle",
|
|
1346
|
+
key: target.key,
|
|
1347
|
+
scope: "global",
|
|
1348
|
+
source,
|
|
1349
|
+
detail: `propagated from ${key}`
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
fireHooks({
|
|
1356
|
+
action: "write",
|
|
1357
|
+
key,
|
|
1358
|
+
scope,
|
|
1359
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1360
|
+
source
|
|
1361
|
+
}, envelope.meta.tags).catch(() => {
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function deleteSecret(key, opts = {}) {
|
|
1365
|
+
const scopes = resolveScope(opts);
|
|
1366
|
+
const source = opts.source ?? "cli";
|
|
1367
|
+
let deleted = false;
|
|
1368
|
+
for (const { service, scope } of scopes) {
|
|
1369
|
+
const entry = new Entry(service, key);
|
|
1370
|
+
try {
|
|
1371
|
+
if (entry.deleteCredential()) {
|
|
1372
|
+
deleted = true;
|
|
1373
|
+
logAudit({ action: "delete", key, scope, source });
|
|
1374
|
+
fireHooks({
|
|
1375
|
+
action: "delete",
|
|
1376
|
+
key,
|
|
1377
|
+
scope,
|
|
1378
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1379
|
+
source
|
|
1380
|
+
}).catch(() => {
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return deleted;
|
|
1387
|
+
}
|
|
1388
|
+
function hasSecret(key, opts = {}) {
|
|
1389
|
+
const scopes = resolveScope(opts);
|
|
1390
|
+
for (const { service } of scopes) {
|
|
1391
|
+
const envelope = readEnvelope(service, key);
|
|
1392
|
+
if (envelope) {
|
|
1393
|
+
const decay = checkDecay(envelope);
|
|
1394
|
+
if (!decay.isExpired) return true;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
function listSecrets(opts = {}) {
|
|
1400
|
+
const source = opts.source ?? "cli";
|
|
1401
|
+
const services = [];
|
|
1402
|
+
if (!opts.scope || opts.scope === "global") {
|
|
1403
|
+
services.push({ service: globalService(), scope: "global" });
|
|
1404
|
+
}
|
|
1405
|
+
if ((!opts.scope || opts.scope === "project") && opts.projectPath) {
|
|
1406
|
+
services.push({
|
|
1407
|
+
service: projectService(opts.projectPath),
|
|
1408
|
+
scope: "project"
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
if ((!opts.scope || opts.scope === "team") && opts.teamId) {
|
|
1412
|
+
services.push({ service: teamService(opts.teamId), scope: "team" });
|
|
1413
|
+
}
|
|
1414
|
+
if ((!opts.scope || opts.scope === "org") && opts.orgId) {
|
|
1415
|
+
services.push({ service: orgService(opts.orgId), scope: "org" });
|
|
1416
|
+
}
|
|
1417
|
+
const results = [];
|
|
1418
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1419
|
+
for (const { service, scope } of services) {
|
|
1420
|
+
try {
|
|
1421
|
+
const credentials = findCredentials(service);
|
|
1422
|
+
for (const cred of credentials) {
|
|
1423
|
+
const id = `${scope}:${cred.account}`;
|
|
1424
|
+
if (seen.has(id)) continue;
|
|
1425
|
+
seen.add(id);
|
|
1426
|
+
const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
|
|
1427
|
+
const decay = checkDecay(envelope);
|
|
1428
|
+
results.push({
|
|
1429
|
+
key: cred.account,
|
|
1430
|
+
scope,
|
|
1431
|
+
envelope,
|
|
1432
|
+
decay
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (!opts.silent) {
|
|
1439
|
+
logAudit({ action: "list", source });
|
|
1440
|
+
}
|
|
1441
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
1442
|
+
}
|
|
1443
|
+
function exportSecrets(opts = {}) {
|
|
1444
|
+
const format = opts.format ?? "env";
|
|
1445
|
+
const env = resolveEnv(opts);
|
|
1446
|
+
let entries = listSecrets(opts);
|
|
1447
|
+
const source = opts.source ?? "cli";
|
|
1448
|
+
if (opts.keys?.length) {
|
|
1449
|
+
const keySet = new Set(opts.keys);
|
|
1450
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
1451
|
+
}
|
|
1452
|
+
if (opts.tags?.length) {
|
|
1453
|
+
entries = entries.filter(
|
|
1454
|
+
(e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
const rawValues = /* @__PURE__ */ new Map();
|
|
1458
|
+
const globalEntries = entries.filter((e) => e.scope === "global");
|
|
1459
|
+
const orgEntries = entries.filter((e) => e.scope === "org");
|
|
1460
|
+
const teamEntries = entries.filter((e) => e.scope === "team");
|
|
1461
|
+
const projectEntries = entries.filter((e) => e.scope === "project");
|
|
1462
|
+
for (const entry of [...globalEntries, ...orgEntries, ...teamEntries, ...projectEntries]) {
|
|
1463
|
+
if (entry.envelope) {
|
|
1464
|
+
const decay = checkDecay(entry.envelope);
|
|
1465
|
+
if (decay.isExpired) continue;
|
|
1466
|
+
const value = collapseValue(entry.envelope, env);
|
|
1467
|
+
if (value !== null) {
|
|
1468
|
+
rawValues.set(entry.key, value);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const [key, value] of rawValues) {
|
|
1474
|
+
try {
|
|
1475
|
+
const resolved = resolveTemplatesOffline(value, rawValues, /* @__PURE__ */ new Set([key]));
|
|
1476
|
+
merged.set(key, resolved);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
console.warn(`Warning: skipped exporting ${key} due to template error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
logAudit({ action: "export", source, detail: `format=${format}` });
|
|
1482
|
+
if (format === "json") {
|
|
1483
|
+
const obj = {};
|
|
1484
|
+
for (const [key, value] of merged) {
|
|
1485
|
+
obj[key] = value;
|
|
1486
|
+
}
|
|
1487
|
+
return JSON.stringify(obj, null, 2);
|
|
1488
|
+
}
|
|
1489
|
+
const lines = [];
|
|
1490
|
+
for (const [key, value] of merged) {
|
|
1491
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1492
|
+
lines.push(`${key}="${escaped}"`);
|
|
1493
|
+
}
|
|
1494
|
+
return lines.join("\n");
|
|
1495
|
+
}
|
|
1496
|
+
function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
1497
|
+
const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
|
|
1498
|
+
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
1499
|
+
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
1500
|
+
const target = { service: targetScopes[0].service, key: targetKey };
|
|
1501
|
+
entangle(source, target);
|
|
1502
|
+
logAudit({
|
|
1503
|
+
action: "entangle",
|
|
1504
|
+
key: sourceKey,
|
|
1505
|
+
source: sourceOpts.source ?? "cli",
|
|
1506
|
+
detail: `entangled with ${targetKey}`
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
1510
|
+
const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
|
|
1511
|
+
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
1512
|
+
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
1513
|
+
const target = { service: targetScopes[0].service, key: targetKey };
|
|
1514
|
+
disentangle(source, target);
|
|
1515
|
+
logAudit({
|
|
1516
|
+
action: "entangle",
|
|
1517
|
+
key: sourceKey,
|
|
1518
|
+
source: sourceOpts.source ?? "cli",
|
|
1519
|
+
detail: `disentangled from ${targetKey}`
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/core/tunnel.ts
|
|
1524
|
+
var tunnelStore = /* @__PURE__ */ new Map();
|
|
1525
|
+
var cleanupInterval = null;
|
|
1526
|
+
function ensureCleanup() {
|
|
1527
|
+
if (cleanupInterval) return;
|
|
1528
|
+
cleanupInterval = setInterval(() => {
|
|
1529
|
+
const now = Date.now();
|
|
1530
|
+
for (const [id, entry] of tunnelStore) {
|
|
1531
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
1532
|
+
tunnelStore.delete(id);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (tunnelStore.size === 0 && cleanupInterval) {
|
|
1536
|
+
clearInterval(cleanupInterval);
|
|
1537
|
+
cleanupInterval = null;
|
|
1538
|
+
}
|
|
1539
|
+
}, 5e3);
|
|
1540
|
+
if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
|
|
1541
|
+
cleanupInterval.unref();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function tunnelCreate(value, opts = {}) {
|
|
1545
|
+
const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1546
|
+
const now = Date.now();
|
|
1547
|
+
tunnelStore.set(id, {
|
|
1548
|
+
value,
|
|
1549
|
+
createdAt: now,
|
|
1550
|
+
expiresAt: opts.ttlSeconds ? now + opts.ttlSeconds * 1e3 : void 0,
|
|
1551
|
+
accessCount: 0,
|
|
1552
|
+
maxReads: opts.maxReads
|
|
1553
|
+
});
|
|
1554
|
+
ensureCleanup();
|
|
1555
|
+
return id;
|
|
1556
|
+
}
|
|
1557
|
+
function tunnelRead(id) {
|
|
1558
|
+
const entry = tunnelStore.get(id);
|
|
1559
|
+
if (!entry) return null;
|
|
1560
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
1561
|
+
tunnelStore.delete(id);
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
entry.accessCount++;
|
|
1565
|
+
if (entry.maxReads && entry.accessCount >= entry.maxReads) {
|
|
1566
|
+
const value = entry.value;
|
|
1567
|
+
tunnelStore.delete(id);
|
|
1568
|
+
return value;
|
|
1569
|
+
}
|
|
1570
|
+
return entry.value;
|
|
1571
|
+
}
|
|
1572
|
+
function tunnelDestroy(id) {
|
|
1573
|
+
return tunnelStore.delete(id);
|
|
1574
|
+
}
|
|
1575
|
+
function tunnelList() {
|
|
1576
|
+
const now = Date.now();
|
|
1577
|
+
const result = [];
|
|
1578
|
+
for (const [id, entry] of tunnelStore) {
|
|
1579
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
1580
|
+
tunnelStore.delete(id);
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
result.push({
|
|
1584
|
+
id,
|
|
1585
|
+
createdAt: entry.createdAt,
|
|
1586
|
+
expiresAt: entry.expiresAt,
|
|
1587
|
+
accessCount: entry.accessCount,
|
|
1588
|
+
maxReads: entry.maxReads
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export {
|
|
1595
|
+
checkDecay,
|
|
1596
|
+
readProjectConfig,
|
|
1597
|
+
collapseEnvironment,
|
|
1598
|
+
logAudit,
|
|
1599
|
+
queryAudit,
|
|
1600
|
+
verifyAuditChain,
|
|
1601
|
+
exportAudit,
|
|
1602
|
+
detectAnomalies,
|
|
1603
|
+
listEntanglements,
|
|
1604
|
+
httpRequest_,
|
|
1605
|
+
registerHook,
|
|
1606
|
+
removeHook,
|
|
1607
|
+
listHooks,
|
|
1608
|
+
enableHook,
|
|
1609
|
+
disableHook,
|
|
1610
|
+
fireHooks,
|
|
1611
|
+
grantApproval,
|
|
1612
|
+
revokeApproval,
|
|
1613
|
+
listApprovals,
|
|
1614
|
+
registry,
|
|
1615
|
+
checkExecPolicy,
|
|
1616
|
+
getExecMaxRuntime,
|
|
1617
|
+
getPolicySummary,
|
|
1618
|
+
getSecret,
|
|
1619
|
+
getEnvelope,
|
|
1620
|
+
setSecret,
|
|
1621
|
+
deleteSecret,
|
|
1622
|
+
hasSecret,
|
|
1623
|
+
listSecrets,
|
|
1624
|
+
exportSecrets,
|
|
1625
|
+
entangleSecrets,
|
|
1626
|
+
disentangleSecrets,
|
|
1627
|
+
tunnelCreate,
|
|
1628
|
+
tunnelRead,
|
|
1629
|
+
tunnelDestroy,
|
|
1630
|
+
tunnelList
|
|
1631
|
+
};
|
|
1632
|
+
//# sourceMappingURL=chunk-WG4ZKN7Q.js.map
|