@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
|
@@ -23,7 +23,9 @@ function createEnvelope(value, opts) {
|
|
|
23
23
|
accessCount: 0,
|
|
24
24
|
rotationFormat: opts?.rotationFormat,
|
|
25
25
|
rotationPrefix: opts?.rotationPrefix,
|
|
26
|
-
provider: opts?.provider
|
|
26
|
+
provider: opts?.provider,
|
|
27
|
+
requiresApproval: opts?.requiresApproval,
|
|
28
|
+
jitProvider: opts?.jitProvider
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
31
|
}
|
|
@@ -204,9 +206,10 @@ function mapEnvName(raw) {
|
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
// src/core/observer.ts
|
|
207
|
-
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2 } from "fs";
|
|
209
|
+
import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
208
210
|
import { join as join2 } from "path";
|
|
209
211
|
import { homedir } from "os";
|
|
212
|
+
import { createHash } from "crypto";
|
|
210
213
|
function getAuditDir() {
|
|
211
214
|
const dir = join2(homedir(), ".config", "q-ring");
|
|
212
215
|
if (!existsSync2(dir)) {
|
|
@@ -217,11 +220,36 @@ function getAuditDir() {
|
|
|
217
220
|
function getAuditPath() {
|
|
218
221
|
return join2(getAuditDir(), "audit.jsonl");
|
|
219
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
|
+
}
|
|
220
246
|
function logAudit(event) {
|
|
247
|
+
const prevHash = getLastLineHash();
|
|
221
248
|
const full = {
|
|
222
249
|
...event,
|
|
223
250
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
224
|
-
pid: process.pid
|
|
251
|
+
pid: process.pid,
|
|
252
|
+
prevHash
|
|
225
253
|
};
|
|
226
254
|
try {
|
|
227
255
|
appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
|
|
@@ -240,35 +268,99 @@ function queryAudit(query = {}) {
|
|
|
240
268
|
return null;
|
|
241
269
|
}
|
|
242
270
|
}).filter((e) => e !== null);
|
|
243
|
-
if (query.key)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (query.
|
|
247
|
-
events = events.filter((e) => e.action === query.action);
|
|
248
|
-
}
|
|
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);
|
|
249
275
|
if (query.since) {
|
|
250
276
|
const since = new Date(query.since).getTime();
|
|
251
|
-
events = events.filter(
|
|
252
|
-
(e) => new Date(e.timestamp).getTime() >= since
|
|
253
|
-
);
|
|
277
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
|
|
254
278
|
}
|
|
255
279
|
events.sort(
|
|
256
280
|
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
257
281
|
);
|
|
258
|
-
if (query.limit)
|
|
259
|
-
events = events.slice(0, query.limit);
|
|
260
|
-
}
|
|
282
|
+
if (query.limit) events = events.slice(0, query.limit);
|
|
261
283
|
return events;
|
|
262
284
|
} catch {
|
|
263
285
|
return [];
|
|
264
286
|
}
|
|
265
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
|
+
}
|
|
266
359
|
function detectAnomalies(key) {
|
|
267
360
|
const recent = queryAudit({
|
|
268
361
|
key,
|
|
269
362
|
action: "read",
|
|
270
363
|
since: new Date(Date.now() - 36e5).toISOString()
|
|
271
|
-
// last hour
|
|
272
364
|
});
|
|
273
365
|
const anomalies = [];
|
|
274
366
|
if (key && recent.length > 50) {
|
|
@@ -289,11 +381,19 @@ function detectAnomalies(key) {
|
|
|
289
381
|
events: nightAccess
|
|
290
382
|
});
|
|
291
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
|
+
}
|
|
292
392
|
return anomalies;
|
|
293
393
|
}
|
|
294
394
|
|
|
295
395
|
// src/core/entanglement.ts
|
|
296
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
396
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
297
397
|
import { join as join3 } from "path";
|
|
298
398
|
import { homedir as homedir2 } from "os";
|
|
299
399
|
function getRegistryPath() {
|
|
@@ -314,38 +414,38 @@ function loadRegistry() {
|
|
|
314
414
|
return { pairs: [] };
|
|
315
415
|
}
|
|
316
416
|
}
|
|
317
|
-
function saveRegistry(
|
|
318
|
-
|
|
417
|
+
function saveRegistry(registry2) {
|
|
418
|
+
writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2));
|
|
319
419
|
}
|
|
320
420
|
function entangle(source, target) {
|
|
321
|
-
const
|
|
322
|
-
const exists =
|
|
421
|
+
const registry2 = loadRegistry();
|
|
422
|
+
const exists = registry2.pairs.some(
|
|
323
423
|
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
324
424
|
);
|
|
325
425
|
if (!exists) {
|
|
326
|
-
|
|
426
|
+
registry2.pairs.push({
|
|
327
427
|
source,
|
|
328
428
|
target,
|
|
329
429
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
330
430
|
});
|
|
331
|
-
|
|
431
|
+
registry2.pairs.push({
|
|
332
432
|
source: target,
|
|
333
433
|
target: source,
|
|
334
434
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
335
435
|
});
|
|
336
|
-
saveRegistry(
|
|
436
|
+
saveRegistry(registry2);
|
|
337
437
|
}
|
|
338
438
|
}
|
|
339
439
|
function disentangle(source, target) {
|
|
340
|
-
const
|
|
341
|
-
|
|
440
|
+
const registry2 = loadRegistry();
|
|
441
|
+
registry2.pairs = registry2.pairs.filter(
|
|
342
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)
|
|
343
443
|
);
|
|
344
|
-
saveRegistry(
|
|
444
|
+
saveRegistry(registry2);
|
|
345
445
|
}
|
|
346
446
|
function findEntangled(source) {
|
|
347
|
-
const
|
|
348
|
-
return
|
|
447
|
+
const registry2 = loadRegistry();
|
|
448
|
+
return registry2.pairs.filter(
|
|
349
449
|
(p) => p.source.service === source.service && p.source.key === source.key
|
|
350
450
|
).map((p) => p.target);
|
|
351
451
|
}
|
|
@@ -357,9 +457,9 @@ function listEntanglements() {
|
|
|
357
457
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
358
458
|
|
|
359
459
|
// src/utils/hash.ts
|
|
360
|
-
import { createHash } from "crypto";
|
|
460
|
+
import { createHash as createHash2 } from "crypto";
|
|
361
461
|
function hashProjectPath(projectPath) {
|
|
362
|
-
return
|
|
462
|
+
return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
363
463
|
}
|
|
364
464
|
|
|
365
465
|
// src/core/scope.ts
|
|
@@ -371,36 +471,133 @@ function projectService(projectPath) {
|
|
|
371
471
|
const hash = hashProjectPath(projectPath);
|
|
372
472
|
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
373
473
|
}
|
|
474
|
+
function teamService(teamId) {
|
|
475
|
+
return `${SERVICE_PREFIX}:team:${teamId}`;
|
|
476
|
+
}
|
|
477
|
+
function orgService(orgId) {
|
|
478
|
+
return `${SERVICE_PREFIX}:org:${orgId}`;
|
|
479
|
+
}
|
|
374
480
|
function resolveScope(opts) {
|
|
375
|
-
const { scope, projectPath } = opts;
|
|
481
|
+
const { scope, projectPath, teamId, orgId } = opts;
|
|
376
482
|
if (scope === "global") {
|
|
377
483
|
return [{ scope: "global", service: globalService() }];
|
|
378
484
|
}
|
|
379
485
|
if (scope === "project") {
|
|
380
|
-
if (!projectPath)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
];
|
|
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 }];
|
|
386
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 = [];
|
|
387
498
|
if (projectPath) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 });
|
|
392
506
|
}
|
|
393
|
-
|
|
507
|
+
chain.push({ scope: "global", service: globalService() });
|
|
508
|
+
return chain;
|
|
394
509
|
}
|
|
395
510
|
|
|
396
511
|
// src/core/hooks.ts
|
|
397
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
512
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
398
513
|
import { join as join4 } from "path";
|
|
399
514
|
import { homedir as homedir3 } from "os";
|
|
400
515
|
import { exec } from "child_process";
|
|
516
|
+
import { randomUUID } from "crypto";
|
|
517
|
+
import { lookup } from "dns/promises";
|
|
518
|
+
|
|
519
|
+
// src/utils/http-request.ts
|
|
401
520
|
import { request as httpsRequest } from "https";
|
|
402
521
|
import { request as httpRequest } from "http";
|
|
403
|
-
|
|
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
|
|
404
601
|
function getRegistryPath2() {
|
|
405
602
|
const dir = join4(homedir3(), ".config", "q-ring");
|
|
406
603
|
if (!existsSync4(dir)) {
|
|
@@ -419,26 +616,26 @@ function loadRegistry2() {
|
|
|
419
616
|
return { hooks: [] };
|
|
420
617
|
}
|
|
421
618
|
}
|
|
422
|
-
function saveRegistry2(
|
|
423
|
-
|
|
619
|
+
function saveRegistry2(registry2) {
|
|
620
|
+
writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2));
|
|
424
621
|
}
|
|
425
622
|
function registerHook(entry) {
|
|
426
|
-
const
|
|
623
|
+
const registry2 = loadRegistry2();
|
|
427
624
|
const hook = {
|
|
428
625
|
...entry,
|
|
429
626
|
id: randomUUID().slice(0, 8),
|
|
430
627
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
431
628
|
};
|
|
432
|
-
|
|
433
|
-
saveRegistry2(
|
|
629
|
+
registry2.hooks.push(hook);
|
|
630
|
+
saveRegistry2(registry2);
|
|
434
631
|
return hook;
|
|
435
632
|
}
|
|
436
633
|
function removeHook(id) {
|
|
437
|
-
const
|
|
438
|
-
const before =
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
saveRegistry2(
|
|
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);
|
|
442
639
|
return true;
|
|
443
640
|
}
|
|
444
641
|
return false;
|
|
@@ -446,22 +643,6 @@ function removeHook(id) {
|
|
|
446
643
|
function listHooks() {
|
|
447
644
|
return loadRegistry2().hooks;
|
|
448
645
|
}
|
|
449
|
-
function enableHook(id) {
|
|
450
|
-
const registry = loadRegistry2();
|
|
451
|
-
const hook = registry.hooks.find((h) => h.id === id);
|
|
452
|
-
if (!hook) return false;
|
|
453
|
-
hook.enabled = true;
|
|
454
|
-
saveRegistry2(registry);
|
|
455
|
-
return true;
|
|
456
|
-
}
|
|
457
|
-
function disableHook(id) {
|
|
458
|
-
const registry = loadRegistry2();
|
|
459
|
-
const hook = registry.hooks.find((h) => h.id === id);
|
|
460
|
-
if (!hook) return false;
|
|
461
|
-
hook.enabled = false;
|
|
462
|
-
saveRegistry2(registry);
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
646
|
function matchesHook(hook, payload, tags) {
|
|
466
647
|
if (!hook.enabled) return false;
|
|
467
648
|
const m = hook.match;
|
|
@@ -495,44 +676,76 @@ function executeShell(command, payload) {
|
|
|
495
676
|
});
|
|
496
677
|
});
|
|
497
678
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
679
|
+
function isPrivateIP(ip) {
|
|
680
|
+
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
681
|
+
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
682
|
+
const ipv4Mapped = ip.match(ipv4Re);
|
|
683
|
+
if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
|
|
684
|
+
if (/^127\./.test(ip)) return true;
|
|
685
|
+
if (/^10\./.test(ip)) return true;
|
|
686
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
687
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
688
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
689
|
+
if (ip === "0.0.0.0") return true;
|
|
690
|
+
if (ip === "::1" || ip === "::") return true;
|
|
691
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
|
|
692
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
async function checkSSRF(url) {
|
|
696
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
697
|
+
try {
|
|
698
|
+
const parsed = new URL(url);
|
|
699
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
700
|
+
if (isPrivateIP(hostname)) {
|
|
701
|
+
return `Blocked: hook URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
702
|
+
}
|
|
703
|
+
const results = await lookup(hostname, { all: true });
|
|
704
|
+
for (const { address } of results) {
|
|
705
|
+
if (isPrivateIP(address)) {
|
|
706
|
+
return `Blocked: hook URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
async function executeHttp(url, payload) {
|
|
714
|
+
const ssrfBlock = await checkSSRF(url);
|
|
715
|
+
if (ssrfBlock) {
|
|
716
|
+
logAudit({
|
|
717
|
+
action: "policy_deny",
|
|
718
|
+
key: payload.key,
|
|
719
|
+
scope: payload.scope,
|
|
720
|
+
source: payload.source,
|
|
721
|
+
detail: `hook SSRF blocked: ${url}`
|
|
722
|
+
});
|
|
723
|
+
return { hookId: "", success: false, message: ssrfBlock };
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
500
726
|
const body = JSON.stringify(payload);
|
|
501
|
-
const
|
|
502
|
-
const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
503
|
-
const req = reqFn(
|
|
727
|
+
const res = await httpRequest_({
|
|
504
728
|
url,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
"Content-Length": Buffer.byteLength(body),
|
|
510
|
-
"User-Agent": "q-ring-hooks/1.0"
|
|
511
|
-
},
|
|
512
|
-
timeout: 1e4
|
|
729
|
+
method: "POST",
|
|
730
|
+
headers: {
|
|
731
|
+
"Content-Type": "application/json",
|
|
732
|
+
"User-Agent": "q-ring-hooks/1.0"
|
|
513
733
|
},
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
res.on("data", (chunk) => data += chunk);
|
|
517
|
-
res.on("end", () => {
|
|
518
|
-
resolve({
|
|
519
|
-
hookId: "",
|
|
520
|
-
success: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
|
|
521
|
-
message: `HTTP ${res.statusCode}`
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
);
|
|
526
|
-
req.on("error", (err) => {
|
|
527
|
-
resolve({ hookId: "", success: false, message: `HTTP error: ${err.message}` });
|
|
528
|
-
});
|
|
529
|
-
req.on("timeout", () => {
|
|
530
|
-
req.destroy();
|
|
531
|
-
resolve({ hookId: "", success: false, message: "HTTP timeout" });
|
|
734
|
+
body,
|
|
735
|
+
timeoutMs: 1e4
|
|
532
736
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
737
|
+
return {
|
|
738
|
+
hookId: "",
|
|
739
|
+
success: res.statusCode >= 200 && res.statusCode < 300,
|
|
740
|
+
message: `HTTP ${res.statusCode}`
|
|
741
|
+
};
|
|
742
|
+
} catch (err) {
|
|
743
|
+
return {
|
|
744
|
+
hookId: "",
|
|
745
|
+
success: false,
|
|
746
|
+
message: err instanceof Error ? err.message : "HTTP error"
|
|
747
|
+
};
|
|
748
|
+
}
|
|
536
749
|
}
|
|
537
750
|
function executeSignal(target, signal = "SIGHUP") {
|
|
538
751
|
return new Promise((resolve) => {
|
|
@@ -616,6 +829,279 @@ async function fireHooks(payload, tags) {
|
|
|
616
829
|
return hookResults;
|
|
617
830
|
}
|
|
618
831
|
|
|
832
|
+
// src/core/approval.ts
|
|
833
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
834
|
+
import { join as join5 } from "path";
|
|
835
|
+
import { homedir as homedir4 } from "os";
|
|
836
|
+
import { createHmac, randomBytes } from "crypto";
|
|
837
|
+
function getHmacSecret() {
|
|
838
|
+
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
839
|
+
const dir = join5(homedir4(), ".config", "q-ring");
|
|
840
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
|
|
841
|
+
if (existsSync5(secretPath)) {
|
|
842
|
+
return readFileSync5(secretPath, "utf8").trim();
|
|
843
|
+
}
|
|
844
|
+
const secret = randomBytes(32).toString("hex");
|
|
845
|
+
writeFileSync4(secretPath, secret, { mode: 384 });
|
|
846
|
+
return secret;
|
|
847
|
+
}
|
|
848
|
+
function computeHmac(entry) {
|
|
849
|
+
const payload = `${entry.id}|${entry.key}|${entry.scope}|${entry.reason}|${entry.grantedBy}|${entry.grantedAt}|${entry.expiresAt}`;
|
|
850
|
+
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
851
|
+
}
|
|
852
|
+
function verifyHmac(entry) {
|
|
853
|
+
const expected = computeHmac(entry);
|
|
854
|
+
return expected === entry.hmac;
|
|
855
|
+
}
|
|
856
|
+
function getRegistryPath3() {
|
|
857
|
+
const dir = join5(homedir4(), ".config", "q-ring");
|
|
858
|
+
if (!existsSync5(dir)) {
|
|
859
|
+
mkdirSync4(dir, { recursive: true });
|
|
860
|
+
}
|
|
861
|
+
return join5(dir, "approvals.json");
|
|
862
|
+
}
|
|
863
|
+
function loadRegistry3() {
|
|
864
|
+
const path = getRegistryPath3();
|
|
865
|
+
if (!existsSync5(path)) {
|
|
866
|
+
return { approvals: [] };
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
870
|
+
} catch {
|
|
871
|
+
return { approvals: [] };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function hasApproval(key, scope) {
|
|
875
|
+
const registry2 = loadRegistry3();
|
|
876
|
+
const entry = registry2.approvals.find(
|
|
877
|
+
(a) => a.key === key && a.scope === scope
|
|
878
|
+
);
|
|
879
|
+
if (!entry) return false;
|
|
880
|
+
if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
|
|
881
|
+
if (!verifyHmac(entry)) return false;
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/core/provision.ts
|
|
886
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
887
|
+
var ProvisionRegistry = class {
|
|
888
|
+
providers = /* @__PURE__ */ new Map();
|
|
889
|
+
register(provider) {
|
|
890
|
+
this.providers.set(provider.name, provider);
|
|
891
|
+
}
|
|
892
|
+
get(name) {
|
|
893
|
+
return this.providers.get(name);
|
|
894
|
+
}
|
|
895
|
+
listProviders() {
|
|
896
|
+
return [...this.providers.values()];
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var awsStsProvider = {
|
|
900
|
+
name: "aws-sts",
|
|
901
|
+
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
902
|
+
provision(configRaw) {
|
|
903
|
+
let config;
|
|
904
|
+
try {
|
|
905
|
+
config = JSON.parse(configRaw);
|
|
906
|
+
} catch {
|
|
907
|
+
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
908
|
+
}
|
|
909
|
+
const roleArn = config.roleArn;
|
|
910
|
+
const sessionName = config.sessionName || "q-ring-agent";
|
|
911
|
+
const duration = config.durationSeconds || 3600;
|
|
912
|
+
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
913
|
+
try {
|
|
914
|
+
const output = execFileSync("aws", [
|
|
915
|
+
"sts",
|
|
916
|
+
"assume-role",
|
|
917
|
+
"--role-arn",
|
|
918
|
+
roleArn,
|
|
919
|
+
"--role-session-name",
|
|
920
|
+
sessionName,
|
|
921
|
+
"--duration-seconds",
|
|
922
|
+
String(duration),
|
|
923
|
+
"--output",
|
|
924
|
+
"json"
|
|
925
|
+
], { encoding: "utf8" });
|
|
926
|
+
const parsed = JSON.parse(output);
|
|
927
|
+
const creds = parsed.Credentials;
|
|
928
|
+
const value = JSON.stringify({
|
|
929
|
+
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
930
|
+
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
931
|
+
AWS_SESSION_TOKEN: creds.SessionToken
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
value,
|
|
935
|
+
expiresAt: creds.Expiration
|
|
936
|
+
};
|
|
937
|
+
} catch (err) {
|
|
938
|
+
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
var httpProvider = {
|
|
943
|
+
name: "http",
|
|
944
|
+
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
945
|
+
provision(configRaw) {
|
|
946
|
+
let config;
|
|
947
|
+
try {
|
|
948
|
+
config = JSON.parse(configRaw);
|
|
949
|
+
} catch {
|
|
950
|
+
throw new Error("http provider requires valid JSON config");
|
|
951
|
+
}
|
|
952
|
+
const url = config.url;
|
|
953
|
+
const method = config.method || "POST";
|
|
954
|
+
const valuePath = config.valuePath || "token";
|
|
955
|
+
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
956
|
+
if (!url) throw new Error("http provider requires url in config");
|
|
957
|
+
const headers = {
|
|
958
|
+
"User-Agent": "q-ring-jit/1.0",
|
|
959
|
+
...config.headers ?? {}
|
|
960
|
+
};
|
|
961
|
+
let bodyStr;
|
|
962
|
+
if (config.body) {
|
|
963
|
+
headers["Content-Type"] = "application/json";
|
|
964
|
+
bodyStr = JSON.stringify(config.body);
|
|
965
|
+
}
|
|
966
|
+
const scriptConfig = JSON.stringify({ url, method, headers, bodyStr });
|
|
967
|
+
const script = `
|
|
968
|
+
const cfg = JSON.parse(process.env.__QRING_HTTP_CFG);
|
|
969
|
+
const parsedUrl = new URL(cfg.url);
|
|
970
|
+
const http = require(parsedUrl.protocol === "https:" ? "node:https" : "node:http");
|
|
971
|
+
const req = http.request(cfg.url, { method: cfg.method, headers: cfg.headers, timeout: 30000 }, (res) => {
|
|
972
|
+
let body = "";
|
|
973
|
+
res.on("data", (chunk) => body += chunk);
|
|
974
|
+
res.on("end", () => process.stdout.write(body));
|
|
975
|
+
});
|
|
976
|
+
req.on("error", (e) => { process.stderr.write(e.message); process.exit(1); });
|
|
977
|
+
if (cfg.bodyStr) req.write(cfg.bodyStr);
|
|
978
|
+
req.end();
|
|
979
|
+
`;
|
|
980
|
+
try {
|
|
981
|
+
const result = spawnSync("node", ["-e", script], {
|
|
982
|
+
encoding: "utf8",
|
|
983
|
+
timeout: 35e3,
|
|
984
|
+
env: { ...process.env, __QRING_HTTP_CFG: scriptConfig }
|
|
985
|
+
});
|
|
986
|
+
if (result.status !== 0) {
|
|
987
|
+
throw new Error(result.stderr || "HTTP request failed");
|
|
988
|
+
}
|
|
989
|
+
const parsed = JSON.parse(result.stdout);
|
|
990
|
+
let val = parsed;
|
|
991
|
+
for (const key of valuePath.split(".")) {
|
|
992
|
+
val = val[key];
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
value: String(val),
|
|
996
|
+
expiresAt: new Date(Date.now() + expiresInSeconds * 1e3).toISOString()
|
|
997
|
+
};
|
|
998
|
+
} catch (err) {
|
|
999
|
+
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var registry = new ProvisionRegistry();
|
|
1004
|
+
registry.register(awsStsProvider);
|
|
1005
|
+
registry.register(httpProvider);
|
|
1006
|
+
|
|
1007
|
+
// src/core/policy.ts
|
|
1008
|
+
var cachedPolicy = null;
|
|
1009
|
+
function loadPolicy(projectPath) {
|
|
1010
|
+
const pp = projectPath ?? process.cwd();
|
|
1011
|
+
if (cachedPolicy && cachedPolicy.path === pp) {
|
|
1012
|
+
return cachedPolicy.policy;
|
|
1013
|
+
}
|
|
1014
|
+
const config = readProjectConfig(pp);
|
|
1015
|
+
const policy = config?.policy ?? {};
|
|
1016
|
+
cachedPolicy = { path: pp, policy };
|
|
1017
|
+
return policy;
|
|
1018
|
+
}
|
|
1019
|
+
function checkToolPolicy(toolName, projectPath) {
|
|
1020
|
+
const policy = loadPolicy(projectPath);
|
|
1021
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1022
|
+
if (policy.mcp.denyTools?.includes(toolName)) {
|
|
1023
|
+
return {
|
|
1024
|
+
allowed: false,
|
|
1025
|
+
reason: `Tool "${toolName}" is denied by project policy`,
|
|
1026
|
+
policySource: ".q-ring.json policy.mcp.denyTools"
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
if (policy.mcp.allowTools && !policy.mcp.allowTools.includes(toolName)) {
|
|
1030
|
+
return {
|
|
1031
|
+
allowed: false,
|
|
1032
|
+
reason: `Tool "${toolName}" is not in the allowlist`,
|
|
1033
|
+
policySource: ".q-ring.json policy.mcp.allowTools"
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1037
|
+
}
|
|
1038
|
+
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1039
|
+
const policy = loadPolicy(projectPath);
|
|
1040
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1041
|
+
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1042
|
+
return {
|
|
1043
|
+
allowed: false,
|
|
1044
|
+
reason: `Key "${key}" is denied by project policy`,
|
|
1045
|
+
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1049
|
+
return {
|
|
1050
|
+
allowed: false,
|
|
1051
|
+
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1052
|
+
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
if (tags && policy.mcp.deniedTags) {
|
|
1056
|
+
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1057
|
+
if (blocked) {
|
|
1058
|
+
return {
|
|
1059
|
+
allowed: false,
|
|
1060
|
+
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1061
|
+
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1066
|
+
}
|
|
1067
|
+
function checkExecPolicy(command, projectPath) {
|
|
1068
|
+
const policy = loadPolicy(projectPath);
|
|
1069
|
+
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1070
|
+
if (policy.exec.denyCommands) {
|
|
1071
|
+
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1072
|
+
if (denied) {
|
|
1073
|
+
return {
|
|
1074
|
+
allowed: false,
|
|
1075
|
+
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1076
|
+
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (policy.exec.allowCommands) {
|
|
1081
|
+
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1082
|
+
if (!allowed) {
|
|
1083
|
+
return {
|
|
1084
|
+
allowed: false,
|
|
1085
|
+
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1086
|
+
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1091
|
+
}
|
|
1092
|
+
function getExecMaxRuntime(projectPath) {
|
|
1093
|
+
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1094
|
+
}
|
|
1095
|
+
function getPolicySummary(projectPath) {
|
|
1096
|
+
const policy = loadPolicy(projectPath);
|
|
1097
|
+
return {
|
|
1098
|
+
hasMcpPolicy: !!policy.mcp,
|
|
1099
|
+
hasExecPolicy: !!policy.exec,
|
|
1100
|
+
hasSecretPolicy: !!policy.secrets,
|
|
1101
|
+
details: policy
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
619
1105
|
// src/core/keyring.ts
|
|
620
1106
|
function readEnvelope(service, key) {
|
|
621
1107
|
const entry = new Entry(service, key);
|
|
@@ -633,29 +1119,104 @@ function resolveEnv(opts) {
|
|
|
633
1119
|
const result = collapseEnvironment({ projectPath: opts.projectPath });
|
|
634
1120
|
return result?.env;
|
|
635
1121
|
}
|
|
1122
|
+
function resolveTemplates(value, opts, seen) {
|
|
1123
|
+
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1124
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
|
|
1125
|
+
const refKey = refKeyRaw.trim();
|
|
1126
|
+
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1127
|
+
if (refValue === null) {
|
|
1128
|
+
throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
|
|
1129
|
+
}
|
|
1130
|
+
return refValue;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1134
|
+
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1135
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
|
|
1136
|
+
const refKey = refKeyRaw.trim();
|
|
1137
|
+
if (seen.has(refKey)) {
|
|
1138
|
+
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
1139
|
+
}
|
|
1140
|
+
const rawRef = rawValues.get(refKey);
|
|
1141
|
+
if (rawRef === void 0) {
|
|
1142
|
+
throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
|
|
1143
|
+
}
|
|
1144
|
+
const nextSeen = new Set(seen);
|
|
1145
|
+
nextSeen.add(refKey);
|
|
1146
|
+
return resolveTemplatesOffline(rawRef, rawValues, nextSeen);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
636
1149
|
function getSecret(key, opts = {}) {
|
|
637
1150
|
const scopes = resolveScope(opts);
|
|
638
1151
|
const env = resolveEnv(opts);
|
|
639
1152
|
const source = opts.source ?? "cli";
|
|
1153
|
+
const seen = opts._seen ?? /* @__PURE__ */ new Set();
|
|
1154
|
+
if (source === "mcp") {
|
|
1155
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1156
|
+
if (!policyDecision.allowed) {
|
|
1157
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (seen.has(key)) {
|
|
1161
|
+
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${key}`);
|
|
1162
|
+
}
|
|
1163
|
+
const nextSeen = new Set(seen);
|
|
1164
|
+
nextSeen.add(key);
|
|
640
1165
|
for (const { service, scope } of scopes) {
|
|
641
1166
|
const envelope = readEnvelope(service, key);
|
|
642
1167
|
if (!envelope) continue;
|
|
643
1168
|
const decay = checkDecay(envelope);
|
|
644
1169
|
if (decay.isExpired) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1170
|
+
if (!opts.silent) {
|
|
1171
|
+
logAudit({
|
|
1172
|
+
action: "read",
|
|
1173
|
+
key,
|
|
1174
|
+
scope,
|
|
1175
|
+
source,
|
|
1176
|
+
detail: "blocked: secret expired (quantum decay)"
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
652
1179
|
continue;
|
|
653
1180
|
}
|
|
654
|
-
|
|
1181
|
+
if (envelope.meta.requiresApproval && source === "mcp") {
|
|
1182
|
+
if (!hasApproval(key, scope)) {
|
|
1183
|
+
if (!opts.silent) {
|
|
1184
|
+
logAudit({
|
|
1185
|
+
action: "read",
|
|
1186
|
+
key,
|
|
1187
|
+
scope,
|
|
1188
|
+
source,
|
|
1189
|
+
detail: "blocked: requires user approval"
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
let value = collapseValue(envelope, env);
|
|
655
1196
|
if (value === null) continue;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1197
|
+
if (envelope.meta.jitProvider) {
|
|
1198
|
+
const provider = registry.get(envelope.meta.jitProvider);
|
|
1199
|
+
if (provider) {
|
|
1200
|
+
let isExpired = true;
|
|
1201
|
+
if (envelope.states && envelope.states["jit"] && envelope.meta.jitExpiresAt) {
|
|
1202
|
+
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1203
|
+
}
|
|
1204
|
+
if (isExpired) {
|
|
1205
|
+
const result = provider.provision(value);
|
|
1206
|
+
envelope.states = envelope.states ?? {};
|
|
1207
|
+
envelope.states["jit"] = result.value;
|
|
1208
|
+
envelope.meta.jitExpiresAt = result.expiresAt;
|
|
1209
|
+
writeEnvelope(service, key, envelope);
|
|
1210
|
+
}
|
|
1211
|
+
value = envelope.states["jit"];
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
|
|
1215
|
+
if (!opts.silent) {
|
|
1216
|
+
const updated = recordAccess(envelope);
|
|
1217
|
+
writeEnvelope(service, key, updated);
|
|
1218
|
+
logAudit({ action: "read", key, scope, env, source });
|
|
1219
|
+
}
|
|
659
1220
|
return value;
|
|
660
1221
|
}
|
|
661
1222
|
return null;
|
|
@@ -678,6 +1239,8 @@ function setSecret(key, value, opts = {}) {
|
|
|
678
1239
|
const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
|
|
679
1240
|
const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
|
|
680
1241
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1242
|
+
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1243
|
+
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
681
1244
|
if (opts.states) {
|
|
682
1245
|
envelope = createEnvelope("", {
|
|
683
1246
|
states: opts.states,
|
|
@@ -689,7 +1252,9 @@ function setSecret(key, value, opts = {}) {
|
|
|
689
1252
|
entangled: existing?.meta.entangled,
|
|
690
1253
|
rotationFormat: rotFmt,
|
|
691
1254
|
rotationPrefix: rotPfx,
|
|
692
|
-
provider: prov
|
|
1255
|
+
provider: prov,
|
|
1256
|
+
requiresApproval: reqApp,
|
|
1257
|
+
jitProvider: jitProv
|
|
693
1258
|
});
|
|
694
1259
|
} else {
|
|
695
1260
|
envelope = createEnvelope(value, {
|
|
@@ -700,7 +1265,9 @@ function setSecret(key, value, opts = {}) {
|
|
|
700
1265
|
entangled: existing?.meta.entangled,
|
|
701
1266
|
rotationFormat: rotFmt,
|
|
702
1267
|
rotationPrefix: rotPfx,
|
|
703
|
-
provider: prov
|
|
1268
|
+
provider: prov,
|
|
1269
|
+
requiresApproval: reqApp,
|
|
1270
|
+
jitProvider: jitProv
|
|
704
1271
|
});
|
|
705
1272
|
}
|
|
706
1273
|
if (existing) {
|
|
@@ -788,6 +1355,12 @@ function listSecrets(opts = {}) {
|
|
|
788
1355
|
scope: "project"
|
|
789
1356
|
});
|
|
790
1357
|
}
|
|
1358
|
+
if ((!opts.scope || opts.scope === "team") && opts.teamId) {
|
|
1359
|
+
services.push({ service: teamService(opts.teamId), scope: "team" });
|
|
1360
|
+
}
|
|
1361
|
+
if ((!opts.scope || opts.scope === "org") && opts.orgId) {
|
|
1362
|
+
services.push({ service: orgService(opts.orgId), scope: "org" });
|
|
1363
|
+
}
|
|
791
1364
|
const results = [];
|
|
792
1365
|
const seen = /* @__PURE__ */ new Set();
|
|
793
1366
|
for (const { service, scope } of services) {
|
|
@@ -828,19 +1401,30 @@ function exportSecrets(opts = {}) {
|
|
|
828
1401
|
(e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
829
1402
|
);
|
|
830
1403
|
}
|
|
831
|
-
const
|
|
1404
|
+
const rawValues = /* @__PURE__ */ new Map();
|
|
832
1405
|
const globalEntries = entries.filter((e) => e.scope === "global");
|
|
1406
|
+
const orgEntries = entries.filter((e) => e.scope === "org");
|
|
1407
|
+
const teamEntries = entries.filter((e) => e.scope === "team");
|
|
833
1408
|
const projectEntries = entries.filter((e) => e.scope === "project");
|
|
834
|
-
for (const entry of [...globalEntries, ...projectEntries]) {
|
|
1409
|
+
for (const entry of [...globalEntries, ...orgEntries, ...teamEntries, ...projectEntries]) {
|
|
835
1410
|
if (entry.envelope) {
|
|
836
1411
|
const decay = checkDecay(entry.envelope);
|
|
837
1412
|
if (decay.isExpired) continue;
|
|
838
1413
|
const value = collapseValue(entry.envelope, env);
|
|
839
1414
|
if (value !== null) {
|
|
840
|
-
|
|
1415
|
+
rawValues.set(entry.key, value);
|
|
841
1416
|
}
|
|
842
1417
|
}
|
|
843
1418
|
}
|
|
1419
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1420
|
+
for (const [key, value] of rawValues) {
|
|
1421
|
+
try {
|
|
1422
|
+
const resolved = resolveTemplatesOffline(value, rawValues, /* @__PURE__ */ new Set([key]));
|
|
1423
|
+
merged.set(key, resolved);
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
console.warn(`Warning: skipped exporting ${key} due to template error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
844
1428
|
logAudit({ action: "export", source, detail: `format=${format}` });
|
|
845
1429
|
if (format === "json") {
|
|
846
1430
|
const obj = {};
|
|
@@ -960,14 +1544,21 @@ export {
|
|
|
960
1544
|
collapseEnvironment,
|
|
961
1545
|
logAudit,
|
|
962
1546
|
queryAudit,
|
|
1547
|
+
verifyAuditChain,
|
|
1548
|
+
exportAudit,
|
|
963
1549
|
detectAnomalies,
|
|
964
1550
|
listEntanglements,
|
|
1551
|
+
httpRequest_,
|
|
965
1552
|
registerHook,
|
|
966
1553
|
removeHook,
|
|
967
1554
|
listHooks,
|
|
968
|
-
enableHook,
|
|
969
|
-
disableHook,
|
|
970
1555
|
fireHooks,
|
|
1556
|
+
registry,
|
|
1557
|
+
checkToolPolicy,
|
|
1558
|
+
checkKeyReadPolicy,
|
|
1559
|
+
checkExecPolicy,
|
|
1560
|
+
getExecMaxRuntime,
|
|
1561
|
+
getPolicySummary,
|
|
971
1562
|
getSecret,
|
|
972
1563
|
getEnvelope,
|
|
973
1564
|
setSecret,
|
|
@@ -982,4 +1573,4 @@ export {
|
|
|
982
1573
|
tunnelDestroy,
|
|
983
1574
|
tunnelList
|
|
984
1575
|
};
|
|
985
|
-
//# sourceMappingURL=chunk-
|
|
1576
|
+
//# sourceMappingURL=chunk-5JBU7TWN.js.map
|