@i4ctime/q-ring 0.9.8 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/{chunk-JV4JFWV6.js → chunk-A4RZVP3P.js} +669 -223
- package/dist/chunk-A4RZVP3P.js.map +1 -0
- package/dist/{chunk-ZV7LRKBA.js → chunk-MQM4DLPI.js} +693 -241
- package/dist/chunk-MQM4DLPI.js.map +1 -0
- package/dist/dashboard-FSCJDLJX.js +1313 -0
- package/dist/dashboard-FSCJDLJX.js.map +1 -0
- package/dist/dashboard-OXUD62RV.js +1313 -0
- package/dist/dashboard-OXUD62RV.js.map +1 -0
- package/dist/index.js +2455 -1993
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1798 -1661
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -3
- package/dist/chunk-JV4JFWV6.js.map +0 -1
- package/dist/chunk-ZV7LRKBA.js.map +0 -1
- package/dist/dashboard-4H474M2N.js +0 -557
- package/dist/dashboard-4H474M2N.js.map +0 -1
- package/dist/dashboard-5L4ILERJ.js +0 -557
- package/dist/dashboard-5L4ILERJ.js.map +0 -1
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/version.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
function readPackageVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(here, "..", "package.json");
|
|
11
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
12
|
+
const pkg = JSON.parse(raw);
|
|
13
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
14
|
+
} catch {
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var PACKAGE_VERSION = readPackageVersion();
|
|
19
|
+
|
|
3
20
|
// src/core/envelope.ts
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
var EntanglementLinkSchema = z.object({
|
|
23
|
+
service: z.string(),
|
|
24
|
+
key: z.string()
|
|
25
|
+
});
|
|
26
|
+
var SecretMetadataSchema = z.object({
|
|
27
|
+
createdAt: z.string(),
|
|
28
|
+
updatedAt: z.string(),
|
|
29
|
+
expiresAt: z.string().optional(),
|
|
30
|
+
ttlSeconds: z.number().optional(),
|
|
31
|
+
description: z.string().optional(),
|
|
32
|
+
tags: z.array(z.string()).optional(),
|
|
33
|
+
entangled: z.array(EntanglementLinkSchema).optional(),
|
|
34
|
+
accessCount: z.number(),
|
|
35
|
+
lastAccessedAt: z.string().optional(),
|
|
36
|
+
ephemeral: z.boolean().optional(),
|
|
37
|
+
rotationFormat: z.string().optional(),
|
|
38
|
+
rotationPrefix: z.string().optional(),
|
|
39
|
+
provider: z.string().optional(),
|
|
40
|
+
validationUrl: z.string().optional(),
|
|
41
|
+
requiresApproval: z.boolean().optional(),
|
|
42
|
+
jitProvider: z.string().optional(),
|
|
43
|
+
jitExpiresAt: z.string().optional()
|
|
44
|
+
});
|
|
45
|
+
var QuantumEnvelopeSchema = z.object({
|
|
46
|
+
v: z.literal(1),
|
|
47
|
+
value: z.string().optional(),
|
|
48
|
+
states: z.record(z.string(), z.string()).optional(),
|
|
49
|
+
defaultEnv: z.string().optional(),
|
|
50
|
+
meta: SecretMetadataSchema
|
|
51
|
+
});
|
|
4
52
|
function createEnvelope(value, opts) {
|
|
5
53
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6
54
|
let expiresAt = opts?.expiresAt;
|
|
@@ -32,8 +80,9 @@ function createEnvelope(value, opts) {
|
|
|
32
80
|
function parseEnvelope(raw) {
|
|
33
81
|
try {
|
|
34
82
|
const parsed = JSON.parse(raw);
|
|
35
|
-
|
|
36
|
-
|
|
83
|
+
const r = QuantumEnvelopeSchema.safeParse(parsed);
|
|
84
|
+
if (r.success) {
|
|
85
|
+
return r.data;
|
|
37
86
|
}
|
|
38
87
|
} catch {
|
|
39
88
|
}
|
|
@@ -84,6 +133,15 @@ function checkDecay(envelope) {
|
|
|
84
133
|
const now = Date.now();
|
|
85
134
|
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
86
135
|
const created = new Date(envelope.meta.createdAt).getTime();
|
|
136
|
+
if (!Number.isFinite(expires) || !Number.isFinite(created)) {
|
|
137
|
+
return {
|
|
138
|
+
isExpired: true,
|
|
139
|
+
isStale: true,
|
|
140
|
+
lifetimePercent: 100,
|
|
141
|
+
secondsRemaining: null,
|
|
142
|
+
timeRemaining: "invalid date"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
87
145
|
const totalLifetime = expires - created;
|
|
88
146
|
const elapsed = now - created;
|
|
89
147
|
const remaining = expires - now;
|
|
@@ -121,8 +179,8 @@ function recordAccess(envelope) {
|
|
|
121
179
|
|
|
122
180
|
// src/core/collapse.ts
|
|
123
181
|
import { execSync } from "child_process";
|
|
124
|
-
import { existsSync, readFileSync } from "fs";
|
|
125
|
-
import { join } from "path";
|
|
182
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
183
|
+
import { join as join2 } from "path";
|
|
126
184
|
var BRANCH_ENV_MAP = {
|
|
127
185
|
main: "prod",
|
|
128
186
|
master: "prod",
|
|
@@ -149,10 +207,10 @@ function detectGitBranch(cwd) {
|
|
|
149
207
|
}
|
|
150
208
|
}
|
|
151
209
|
function readProjectConfig(projectPath) {
|
|
152
|
-
const configPath =
|
|
210
|
+
const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
|
|
153
211
|
try {
|
|
154
212
|
if (existsSync(configPath)) {
|
|
155
|
-
return JSON.parse(
|
|
213
|
+
return JSON.parse(readFileSync2(configPath, "utf8"));
|
|
156
214
|
}
|
|
157
215
|
} catch {
|
|
158
216
|
}
|
|
@@ -188,12 +246,15 @@ function collapseEnvironment(ctx = {}) {
|
|
|
188
246
|
}
|
|
189
247
|
return null;
|
|
190
248
|
}
|
|
249
|
+
var MAX_BRANCH_GLOB_PATTERN_LEN = 200;
|
|
250
|
+
var MAX_BRANCH_GLOB_REGEX_SOURCE = 400;
|
|
191
251
|
function matchGlob(branchMap, branch) {
|
|
192
252
|
for (const [pattern, env] of Object.entries(branchMap)) {
|
|
193
253
|
if (!pattern.includes("*")) continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
254
|
+
if (pattern.length > MAX_BRANCH_GLOB_PATTERN_LEN) continue;
|
|
255
|
+
const source = "^" + pattern.replace(/\*/g, ".*") + "$";
|
|
256
|
+
if (source.length > MAX_BRANCH_GLOB_REGEX_SOURCE) continue;
|
|
257
|
+
const regex = new RegExp(source);
|
|
197
258
|
if (regex.test(branch)) return env;
|
|
198
259
|
}
|
|
199
260
|
return void 0;
|
|
@@ -206,19 +267,35 @@ function mapEnvName(raw) {
|
|
|
206
267
|
}
|
|
207
268
|
|
|
208
269
|
// src/core/observer.ts
|
|
209
|
-
import {
|
|
210
|
-
|
|
270
|
+
import {
|
|
271
|
+
existsSync as existsSync2,
|
|
272
|
+
mkdirSync,
|
|
273
|
+
appendFileSync,
|
|
274
|
+
readFileSync as readFileSync3,
|
|
275
|
+
openSync,
|
|
276
|
+
fstatSync,
|
|
277
|
+
readSync,
|
|
278
|
+
closeSync,
|
|
279
|
+
statSync
|
|
280
|
+
} from "fs";
|
|
281
|
+
import { join as join3 } from "path";
|
|
211
282
|
import { homedir } from "os";
|
|
212
283
|
import { createHash } from "crypto";
|
|
213
284
|
function getAuditDir() {
|
|
214
|
-
|
|
285
|
+
if (process.env.QRING_AUDIT_DIR) {
|
|
286
|
+
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
287
|
+
mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
return process.env.QRING_AUDIT_DIR;
|
|
290
|
+
}
|
|
291
|
+
const dir = join3(homedir(), ".config", "q-ring");
|
|
215
292
|
if (!existsSync2(dir)) {
|
|
216
293
|
mkdirSync(dir, { recursive: true });
|
|
217
294
|
}
|
|
218
295
|
return dir;
|
|
219
296
|
}
|
|
220
297
|
function getAuditPath() {
|
|
221
|
-
return
|
|
298
|
+
return join3(getAuditDir(), "audit.jsonl");
|
|
222
299
|
}
|
|
223
300
|
function getLastLineHash() {
|
|
224
301
|
const path = getAuditPath();
|
|
@@ -256,11 +333,24 @@ function logAudit(event) {
|
|
|
256
333
|
} catch {
|
|
257
334
|
}
|
|
258
335
|
}
|
|
336
|
+
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
259
337
|
function queryAudit(query = {}) {
|
|
260
338
|
const path = getAuditPath();
|
|
261
339
|
if (!existsSync2(path)) return [];
|
|
262
340
|
try {
|
|
263
|
-
const
|
|
341
|
+
const st = statSync(path);
|
|
342
|
+
const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
|
|
343
|
+
const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
|
|
344
|
+
const buf = Buffer.alloc(readLen);
|
|
345
|
+
const fd = openSync(path, "r");
|
|
346
|
+
readSync(fd, buf, 0, readLen, readStart);
|
|
347
|
+
closeSync(fd);
|
|
348
|
+
let text = buf.toString("utf8");
|
|
349
|
+
if (readStart > 0) {
|
|
350
|
+
const firstNl = text.indexOf("\n");
|
|
351
|
+
if (firstNl !== -1) text = text.slice(firstNl + 1);
|
|
352
|
+
}
|
|
353
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
264
354
|
let events = lines.map((line) => {
|
|
265
355
|
try {
|
|
266
356
|
return JSON.parse(line);
|
|
@@ -290,7 +380,7 @@ function verifyAuditChain() {
|
|
|
290
380
|
if (!existsSync2(path)) {
|
|
291
381
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
292
382
|
}
|
|
293
|
-
const lines =
|
|
383
|
+
const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
|
|
294
384
|
if (lines.length === 0) {
|
|
295
385
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
296
386
|
}
|
|
@@ -328,7 +418,7 @@ function verifyAuditChain() {
|
|
|
328
418
|
function exportAudit(opts = {}) {
|
|
329
419
|
const path = getAuditPath();
|
|
330
420
|
if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
|
|
331
|
-
const lines =
|
|
421
|
+
const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
|
|
332
422
|
let events = lines.map((l) => {
|
|
333
423
|
try {
|
|
334
424
|
return JSON.parse(l);
|
|
@@ -393,15 +483,15 @@ function detectAnomalies(key) {
|
|
|
393
483
|
}
|
|
394
484
|
|
|
395
485
|
// src/core/entanglement.ts
|
|
396
|
-
import { existsSync as existsSync3, readFileSync as
|
|
397
|
-
import { join as
|
|
486
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
487
|
+
import { join as join4 } from "path";
|
|
398
488
|
import { homedir as homedir2 } from "os";
|
|
399
489
|
function getRegistryPath() {
|
|
400
|
-
const dir =
|
|
490
|
+
const dir = join4(homedir2(), ".config", "q-ring");
|
|
401
491
|
if (!existsSync3(dir)) {
|
|
402
492
|
mkdirSync2(dir, { recursive: true });
|
|
403
493
|
}
|
|
404
|
-
return
|
|
494
|
+
return join4(dir, "entanglement.json");
|
|
405
495
|
}
|
|
406
496
|
function loadRegistry() {
|
|
407
497
|
const path = getRegistryPath();
|
|
@@ -409,13 +499,15 @@ function loadRegistry() {
|
|
|
409
499
|
return { pairs: [] };
|
|
410
500
|
}
|
|
411
501
|
try {
|
|
412
|
-
return JSON.parse(
|
|
502
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
413
503
|
} catch {
|
|
414
504
|
return { pairs: [] };
|
|
415
505
|
}
|
|
416
506
|
}
|
|
417
507
|
function saveRegistry(registry2) {
|
|
418
|
-
|
|
508
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry2, null, 2), {
|
|
509
|
+
mode: 384
|
|
510
|
+
});
|
|
419
511
|
}
|
|
420
512
|
function entangle(source, target) {
|
|
421
513
|
const registry2 = loadRegistry();
|
|
@@ -453,74 +545,19 @@ function listEntanglements() {
|
|
|
453
545
|
return loadRegistry().pairs;
|
|
454
546
|
}
|
|
455
547
|
|
|
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
548
|
// src/core/hooks.ts
|
|
512
|
-
import { existsSync as existsSync4, readFileSync as
|
|
513
|
-
import { join as
|
|
549
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
550
|
+
import { join as join5 } from "path";
|
|
514
551
|
import { homedir as homedir3 } from "os";
|
|
515
|
-
import {
|
|
552
|
+
import { execFile, spawn } from "child_process";
|
|
516
553
|
import { randomUUID } from "crypto";
|
|
517
554
|
|
|
518
555
|
// src/utils/http-request.ts
|
|
519
556
|
import { request as httpsRequest } from "https";
|
|
520
|
-
import { request as
|
|
557
|
+
import { request as httpRequestPlain } from "http";
|
|
521
558
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
522
559
|
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
523
|
-
function
|
|
560
|
+
function httpRequest(opts) {
|
|
524
561
|
const {
|
|
525
562
|
url,
|
|
526
563
|
method = "GET",
|
|
@@ -535,7 +572,7 @@ function httpRequest_(opts) {
|
|
|
535
572
|
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
536
573
|
return;
|
|
537
574
|
}
|
|
538
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest :
|
|
575
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
539
576
|
const reqHeaders = { ...headers };
|
|
540
577
|
if (body && !reqHeaders["Content-Length"]) {
|
|
541
578
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
@@ -598,6 +635,19 @@ function httpRequest_(opts) {
|
|
|
598
635
|
|
|
599
636
|
// src/core/ssrf.ts
|
|
600
637
|
import { lookup } from "dns/promises";
|
|
638
|
+
import * as dns from "dns";
|
|
639
|
+
import { isIPv4, isIPv6 } from "net";
|
|
640
|
+
function lookupAddressesSync(hostname2) {
|
|
641
|
+
const lookupSync2 = dns.lookupSync;
|
|
642
|
+
return lookupSync2(hostname2, { all: true });
|
|
643
|
+
}
|
|
644
|
+
function isHostnameIpLiteral(hostname2) {
|
|
645
|
+
if (isIPv4(hostname2)) return true;
|
|
646
|
+
if (hostname2.startsWith("[") && hostname2.endsWith("]")) {
|
|
647
|
+
return isIPv6(hostname2.slice(1, -1));
|
|
648
|
+
}
|
|
649
|
+
return isIPv6(hostname2);
|
|
650
|
+
}
|
|
601
651
|
function isPrivateIP(ip) {
|
|
602
652
|
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
603
653
|
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
@@ -618,40 +668,64 @@ async function checkSSRF(url) {
|
|
|
618
668
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
619
669
|
try {
|
|
620
670
|
const parsed = new URL(url);
|
|
621
|
-
const
|
|
622
|
-
if (isPrivateIP(
|
|
623
|
-
return `Blocked: URL resolves to private address (${
|
|
671
|
+
const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
672
|
+
if (isPrivateIP(hostname2)) {
|
|
673
|
+
return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
624
674
|
}
|
|
625
|
-
const results = await lookup(
|
|
675
|
+
const results = await lookup(hostname2, { all: true });
|
|
626
676
|
for (const { address } of results) {
|
|
627
677
|
if (isPrivateIP(address)) {
|
|
628
|
-
return `Blocked: URL "${
|
|
678
|
+
return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
629
679
|
}
|
|
630
680
|
}
|
|
631
681
|
} catch {
|
|
632
682
|
}
|
|
633
683
|
return null;
|
|
634
684
|
}
|
|
635
|
-
function
|
|
685
|
+
function checkJitHttpProvisionUrl(url) {
|
|
636
686
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
637
687
|
try {
|
|
638
688
|
const parsed = new URL(url);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
689
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
690
|
+
return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
|
|
691
|
+
}
|
|
692
|
+
const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
693
|
+
if (!hostname2) {
|
|
694
|
+
return "Blocked: empty hostname in JIT URL.";
|
|
695
|
+
}
|
|
696
|
+
if (isPrivateIP(hostname2)) {
|
|
697
|
+
return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
698
|
+
}
|
|
699
|
+
if (isHostnameIpLiteral(hostname2)) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
let results;
|
|
703
|
+
try {
|
|
704
|
+
results = lookupAddressesSync(hostname2);
|
|
705
|
+
} catch {
|
|
706
|
+
return `Blocked: DNS resolution failed for "${hostname2}" (JIT HTTP provisioning fails closed).`;
|
|
707
|
+
}
|
|
708
|
+
if (!results.length) {
|
|
709
|
+
return `Blocked: DNS returned no addresses for "${hostname2}".`;
|
|
710
|
+
}
|
|
711
|
+
for (const { address } of results) {
|
|
712
|
+
if (isPrivateIP(address)) {
|
|
713
|
+
return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
714
|
+
}
|
|
642
715
|
}
|
|
643
716
|
} catch {
|
|
717
|
+
return "Blocked: malformed JIT HTTP URL.";
|
|
644
718
|
}
|
|
645
719
|
return null;
|
|
646
720
|
}
|
|
647
721
|
|
|
648
722
|
// src/core/hooks.ts
|
|
649
723
|
function getRegistryPath2() {
|
|
650
|
-
const dir =
|
|
724
|
+
const dir = join5(homedir3(), ".config", "q-ring");
|
|
651
725
|
if (!existsSync4(dir)) {
|
|
652
726
|
mkdirSync3(dir, { recursive: true });
|
|
653
727
|
}
|
|
654
|
-
return
|
|
728
|
+
return join5(dir, "hooks.json");
|
|
655
729
|
}
|
|
656
730
|
function loadRegistry2() {
|
|
657
731
|
const path = getRegistryPath2();
|
|
@@ -659,13 +733,15 @@ function loadRegistry2() {
|
|
|
659
733
|
return { hooks: [] };
|
|
660
734
|
}
|
|
661
735
|
try {
|
|
662
|
-
return JSON.parse(
|
|
736
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
663
737
|
} catch {
|
|
664
738
|
return { hooks: [] };
|
|
665
739
|
}
|
|
666
740
|
}
|
|
667
741
|
function saveRegistry2(registry2) {
|
|
668
|
-
|
|
742
|
+
writeFileSync2(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
|
|
743
|
+
mode: 384
|
|
744
|
+
});
|
|
669
745
|
}
|
|
670
746
|
function registerHook(entry) {
|
|
671
747
|
const registry2 = loadRegistry2();
|
|
@@ -705,6 +781,14 @@ function matchesHook(hook, payload, tags) {
|
|
|
705
781
|
if (m.scope && m.scope !== payload.scope) return false;
|
|
706
782
|
return true;
|
|
707
783
|
}
|
|
784
|
+
var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
785
|
+
function shellRunner() {
|
|
786
|
+
if (process.platform === "win32") {
|
|
787
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
788
|
+
return { file: comspec, args: ["/d", "/s", "/c"] };
|
|
789
|
+
}
|
|
790
|
+
return { file: "/bin/sh", args: ["-c"] };
|
|
791
|
+
}
|
|
708
792
|
function executeShell(command, payload) {
|
|
709
793
|
return new Promise((resolve) => {
|
|
710
794
|
const env = {
|
|
@@ -713,13 +797,33 @@ function executeShell(command, payload) {
|
|
|
713
797
|
QRING_HOOK_ACTION: payload.action,
|
|
714
798
|
QRING_HOOK_SCOPE: payload.scope
|
|
715
799
|
};
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
800
|
+
const { file, args } = shellRunner();
|
|
801
|
+
execFile(
|
|
802
|
+
file,
|
|
803
|
+
[...args, command],
|
|
804
|
+
{
|
|
805
|
+
timeout: 3e4,
|
|
806
|
+
env,
|
|
807
|
+
windowsHide: true,
|
|
808
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
809
|
+
encoding: "utf8"
|
|
810
|
+
},
|
|
811
|
+
(err, stdout) => {
|
|
812
|
+
if (err) {
|
|
813
|
+
resolve({
|
|
814
|
+
hookId: "",
|
|
815
|
+
success: false,
|
|
816
|
+
message: `Shell error: ${err.message}`
|
|
817
|
+
});
|
|
818
|
+
} else {
|
|
819
|
+
resolve({
|
|
820
|
+
hookId: "",
|
|
821
|
+
success: true,
|
|
822
|
+
message: (stdout ?? "").trim() || "OK"
|
|
823
|
+
});
|
|
824
|
+
}
|
|
721
825
|
}
|
|
722
|
-
|
|
826
|
+
);
|
|
723
827
|
});
|
|
724
828
|
}
|
|
725
829
|
async function executeHttp(url, payload) {
|
|
@@ -736,7 +840,7 @@ async function executeHttp(url, payload) {
|
|
|
736
840
|
}
|
|
737
841
|
try {
|
|
738
842
|
const body = JSON.stringify(payload);
|
|
739
|
-
const res = await
|
|
843
|
+
const res = await httpRequest({
|
|
740
844
|
url,
|
|
741
845
|
method: "POST",
|
|
742
846
|
headers: {
|
|
@@ -761,24 +865,48 @@ async function executeHttp(url, payload) {
|
|
|
761
865
|
}
|
|
762
866
|
function executeSignal(target, signal = "SIGHUP") {
|
|
763
867
|
return new Promise((resolve) => {
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
868
|
+
const trimmed = target.trim();
|
|
869
|
+
if (/^\d+$/.test(trimmed)) {
|
|
870
|
+
const pid = parseInt(trimmed, 10);
|
|
766
871
|
try {
|
|
767
872
|
process.kill(pid, signal);
|
|
768
|
-
resolve({
|
|
873
|
+
resolve({
|
|
874
|
+
hookId: "",
|
|
875
|
+
success: true,
|
|
876
|
+
message: `Signal ${signal} sent to PID ${pid}`
|
|
877
|
+
});
|
|
769
878
|
} catch (err) {
|
|
770
|
-
resolve({
|
|
879
|
+
resolve({
|
|
880
|
+
hookId: "",
|
|
881
|
+
success: false,
|
|
882
|
+
message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
|
|
883
|
+
});
|
|
771
884
|
}
|
|
772
885
|
return;
|
|
773
886
|
}
|
|
774
|
-
|
|
887
|
+
if (!SAFE_PGREP_NAME.test(trimmed)) {
|
|
888
|
+
resolve({
|
|
889
|
+
hookId: "",
|
|
890
|
+
success: false,
|
|
891
|
+
message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const child = spawn("pgrep", ["-f", trimmed], {
|
|
896
|
+
timeout: 5e3,
|
|
897
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
898
|
+
});
|
|
775
899
|
let stdout = "";
|
|
776
900
|
child.stdout.on("data", (d) => {
|
|
777
901
|
stdout += d.toString();
|
|
778
902
|
});
|
|
779
903
|
child.on("close", (code) => {
|
|
780
904
|
if (code !== 0 || !stdout.trim()) {
|
|
781
|
-
resolve({
|
|
905
|
+
resolve({
|
|
906
|
+
hookId: "",
|
|
907
|
+
success: false,
|
|
908
|
+
message: `Process "${trimmed}" not found`
|
|
909
|
+
});
|
|
782
910
|
return;
|
|
783
911
|
}
|
|
784
912
|
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
@@ -790,10 +918,18 @@ function executeSignal(target, signal = "SIGHUP") {
|
|
|
790
918
|
} catch {
|
|
791
919
|
}
|
|
792
920
|
}
|
|
793
|
-
resolve({
|
|
921
|
+
resolve({
|
|
922
|
+
hookId: "",
|
|
923
|
+
success: sent > 0,
|
|
924
|
+
message: `Signal ${signal} sent to ${sent} process(es)`
|
|
925
|
+
});
|
|
794
926
|
});
|
|
795
927
|
child.on("error", () => {
|
|
796
|
-
resolve({
|
|
928
|
+
resolve({
|
|
929
|
+
hookId: "",
|
|
930
|
+
success: false,
|
|
931
|
+
message: `Process "${trimmed}" not found`
|
|
932
|
+
});
|
|
797
933
|
});
|
|
798
934
|
});
|
|
799
935
|
}
|
|
@@ -850,35 +986,52 @@ async function fireHooks(payload, tags) {
|
|
|
850
986
|
}
|
|
851
987
|
|
|
852
988
|
// src/core/approval.ts
|
|
853
|
-
import { existsSync as existsSync5, readFileSync as
|
|
854
|
-
import { join as
|
|
989
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
990
|
+
import { join as join6 } from "path";
|
|
855
991
|
import { homedir as homedir4 } from "os";
|
|
856
|
-
import { createHmac, randomBytes } from "crypto";
|
|
992
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
857
993
|
function getHmacSecret() {
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
|
|
994
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
995
|
+
const secretPath = join6(dir, ".approval-key");
|
|
996
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
861
997
|
if (existsSync5(secretPath)) {
|
|
862
|
-
return
|
|
998
|
+
return readFileSync6(secretPath, "utf8").trim();
|
|
863
999
|
}
|
|
864
1000
|
const secret = randomBytes(32).toString("hex");
|
|
865
|
-
|
|
1001
|
+
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
866
1002
|
return secret;
|
|
867
1003
|
}
|
|
868
1004
|
function computeHmac(entry) {
|
|
869
|
-
const payload =
|
|
1005
|
+
const payload = [
|
|
1006
|
+
entry.id,
|
|
1007
|
+
entry.key,
|
|
1008
|
+
entry.scope,
|
|
1009
|
+
entry.reason,
|
|
1010
|
+
entry.grantedBy,
|
|
1011
|
+
entry.grantedAt,
|
|
1012
|
+
entry.expiresAt,
|
|
1013
|
+
entry.workspace ?? "",
|
|
1014
|
+
entry.sessionId ?? ""
|
|
1015
|
+
].join("|");
|
|
870
1016
|
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
871
1017
|
}
|
|
872
1018
|
function verifyHmac(entry) {
|
|
873
1019
|
const expected = computeHmac(entry);
|
|
874
|
-
|
|
1020
|
+
const a = Buffer.from(expected, "utf8");
|
|
1021
|
+
const b = Buffer.from(entry.hmac, "utf8");
|
|
1022
|
+
if (a.length !== b.length) return false;
|
|
1023
|
+
try {
|
|
1024
|
+
return timingSafeEqual(a, b);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
875
1028
|
}
|
|
876
1029
|
function getRegistryPath3() {
|
|
877
|
-
const dir =
|
|
1030
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
878
1031
|
if (!existsSync5(dir)) {
|
|
879
|
-
mkdirSync4(dir, { recursive: true });
|
|
1032
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
880
1033
|
}
|
|
881
|
-
return
|
|
1034
|
+
return join6(dir, "approvals.json");
|
|
882
1035
|
}
|
|
883
1036
|
function loadRegistry3() {
|
|
884
1037
|
const path = getRegistryPath3();
|
|
@@ -886,7 +1039,7 @@ function loadRegistry3() {
|
|
|
886
1039
|
return { approvals: [] };
|
|
887
1040
|
}
|
|
888
1041
|
try {
|
|
889
|
-
return JSON.parse(
|
|
1042
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
890
1043
|
} catch {
|
|
891
1044
|
return { approvals: [] };
|
|
892
1045
|
}
|
|
@@ -901,9 +1054,221 @@ function hasApproval(key, scope) {
|
|
|
901
1054
|
if (!verifyHmac(entry)) return false;
|
|
902
1055
|
return true;
|
|
903
1056
|
}
|
|
1057
|
+
function listApprovals() {
|
|
1058
|
+
const registry2 = loadRegistry3();
|
|
1059
|
+
const now = Date.now();
|
|
1060
|
+
return registry2.approvals.map((a) => ({
|
|
1061
|
+
...a,
|
|
1062
|
+
valid: new Date(a.expiresAt).getTime() > now,
|
|
1063
|
+
tampered: !verifyHmac(a)
|
|
1064
|
+
}));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/core/policy.ts
|
|
1068
|
+
var cachedPolicy = null;
|
|
1069
|
+
function loadPolicy(projectPath) {
|
|
1070
|
+
const pp = projectPath ?? process.cwd();
|
|
1071
|
+
if (cachedPolicy && cachedPolicy.path === pp) {
|
|
1072
|
+
return cachedPolicy.policy;
|
|
1073
|
+
}
|
|
1074
|
+
const config = readProjectConfig(pp);
|
|
1075
|
+
const policy = config?.policy ?? {};
|
|
1076
|
+
cachedPolicy = { path: pp, policy };
|
|
1077
|
+
return policy;
|
|
1078
|
+
}
|
|
1079
|
+
function checkToolPolicy(toolName, projectPath) {
|
|
1080
|
+
const policy = loadPolicy(projectPath);
|
|
1081
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1082
|
+
if (policy.mcp.denyTools?.includes(toolName)) {
|
|
1083
|
+
return {
|
|
1084
|
+
allowed: false,
|
|
1085
|
+
reason: `Tool "${toolName}" is denied by project policy`,
|
|
1086
|
+
policySource: ".q-ring.json policy.mcp.denyTools"
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (policy.mcp.allowTools && !policy.mcp.allowTools.includes(toolName)) {
|
|
1090
|
+
return {
|
|
1091
|
+
allowed: false,
|
|
1092
|
+
reason: `Tool "${toolName}" is not in the allowlist`,
|
|
1093
|
+
policySource: ".q-ring.json policy.mcp.allowTools"
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1097
|
+
}
|
|
1098
|
+
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1099
|
+
const policy = loadPolicy(projectPath);
|
|
1100
|
+
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1101
|
+
const s = policy.secrets;
|
|
1102
|
+
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1103
|
+
return {
|
|
1104
|
+
allowed: false,
|
|
1105
|
+
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1106
|
+
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1110
|
+
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1111
|
+
if (hit && !input.requiresApproval) {
|
|
1112
|
+
return {
|
|
1113
|
+
allowed: false,
|
|
1114
|
+
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1115
|
+
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1120
|
+
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1121
|
+
if (hit && !input.rotationFormat) {
|
|
1122
|
+
return {
|
|
1123
|
+
allowed: false,
|
|
1124
|
+
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1125
|
+
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1130
|
+
}
|
|
1131
|
+
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1132
|
+
const policy = loadPolicy(projectPath);
|
|
1133
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1134
|
+
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1135
|
+
return {
|
|
1136
|
+
allowed: false,
|
|
1137
|
+
reason: `Key "${key}" is denied by project policy`,
|
|
1138
|
+
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1142
|
+
return {
|
|
1143
|
+
allowed: false,
|
|
1144
|
+
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1145
|
+
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (tags && policy.mcp.deniedTags) {
|
|
1149
|
+
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1150
|
+
if (blocked) {
|
|
1151
|
+
return {
|
|
1152
|
+
allowed: false,
|
|
1153
|
+
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1154
|
+
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1159
|
+
}
|
|
1160
|
+
function checkExecPolicy(command, projectPath) {
|
|
1161
|
+
const policy = loadPolicy(projectPath);
|
|
1162
|
+
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1163
|
+
if (policy.exec.denyCommands) {
|
|
1164
|
+
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1165
|
+
if (denied) {
|
|
1166
|
+
return {
|
|
1167
|
+
allowed: false,
|
|
1168
|
+
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1169
|
+
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (policy.exec.allowCommands) {
|
|
1174
|
+
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1175
|
+
if (!allowed) {
|
|
1176
|
+
return {
|
|
1177
|
+
allowed: false,
|
|
1178
|
+
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1179
|
+
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1184
|
+
}
|
|
1185
|
+
function getExecMaxRuntime(projectPath) {
|
|
1186
|
+
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1187
|
+
}
|
|
1188
|
+
function getPolicySummary(projectPath) {
|
|
1189
|
+
const policy = loadPolicy(projectPath);
|
|
1190
|
+
return {
|
|
1191
|
+
hasMcpPolicy: !!policy.mcp,
|
|
1192
|
+
hasExecPolicy: !!policy.exec,
|
|
1193
|
+
hasSecretPolicy: !!policy.secrets,
|
|
1194
|
+
details: policy
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/core/keyring.ts
|
|
1199
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1200
|
+
import { homedir as homedir5 } from "os";
|
|
1201
|
+
import { join as join7 } from "path";
|
|
1202
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1203
|
+
|
|
1204
|
+
// src/utils/hash.ts
|
|
1205
|
+
import { createHash as createHash2 } from "crypto";
|
|
1206
|
+
function hashProjectPath(projectPath) {
|
|
1207
|
+
return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/core/scope.ts
|
|
1211
|
+
var SERVICE_PREFIX = "q-ring";
|
|
1212
|
+
function globalService() {
|
|
1213
|
+
return `${SERVICE_PREFIX}:global`;
|
|
1214
|
+
}
|
|
1215
|
+
function projectService(projectPath) {
|
|
1216
|
+
const hash = hashProjectPath(projectPath);
|
|
1217
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
1218
|
+
}
|
|
1219
|
+
function teamService(teamId) {
|
|
1220
|
+
return `${SERVICE_PREFIX}:team:${teamId}`;
|
|
1221
|
+
}
|
|
1222
|
+
function orgService(orgId) {
|
|
1223
|
+
return `${SERVICE_PREFIX}:org:${orgId}`;
|
|
1224
|
+
}
|
|
1225
|
+
function resolveScope(opts) {
|
|
1226
|
+
const { scope, projectPath, teamId, orgId } = opts;
|
|
1227
|
+
if (scope === "global") {
|
|
1228
|
+
return [{ scope: "global", service: globalService() }];
|
|
1229
|
+
}
|
|
1230
|
+
if (scope === "project") {
|
|
1231
|
+
if (!projectPath) throw new Error("Project path is required for project scope");
|
|
1232
|
+
return [{ scope: "project", service: projectService(projectPath), projectPath }];
|
|
1233
|
+
}
|
|
1234
|
+
if (scope === "team") {
|
|
1235
|
+
if (!teamId) throw new Error("Team ID is required for team scope");
|
|
1236
|
+
return [{ scope: "team", service: teamService(teamId), teamId }];
|
|
1237
|
+
}
|
|
1238
|
+
if (scope === "org") {
|
|
1239
|
+
if (!orgId) throw new Error("Org ID is required for org scope");
|
|
1240
|
+
return [{ scope: "org", service: orgService(orgId), orgId }];
|
|
1241
|
+
}
|
|
1242
|
+
const chain = [];
|
|
1243
|
+
if (projectPath) {
|
|
1244
|
+
chain.push({ scope: "project", service: projectService(projectPath), projectPath });
|
|
1245
|
+
}
|
|
1246
|
+
if (teamId) {
|
|
1247
|
+
chain.push({ scope: "team", service: teamService(teamId), teamId });
|
|
1248
|
+
}
|
|
1249
|
+
if (orgId) {
|
|
1250
|
+
chain.push({ scope: "org", service: orgService(orgId), orgId });
|
|
1251
|
+
}
|
|
1252
|
+
chain.push({ scope: "global", service: globalService() });
|
|
1253
|
+
return chain;
|
|
1254
|
+
}
|
|
904
1255
|
|
|
905
1256
|
// src/core/provision.ts
|
|
906
1257
|
import { execFileSync, spawnSync } from "child_process";
|
|
1258
|
+
import { z as z2 } from "zod";
|
|
1259
|
+
var AwsStsConfigSchema = z2.object({
|
|
1260
|
+
roleArn: z2.string(),
|
|
1261
|
+
sessionName: z2.string().optional(),
|
|
1262
|
+
durationSeconds: z2.number().optional()
|
|
1263
|
+
});
|
|
1264
|
+
var HttpJitConfigSchema = z2.object({
|
|
1265
|
+
url: z2.string(),
|
|
1266
|
+
method: z2.string().optional(),
|
|
1267
|
+
valuePath: z2.string().optional(),
|
|
1268
|
+
expiresInSeconds: z2.number().optional(),
|
|
1269
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1270
|
+
body: z2.unknown().optional()
|
|
1271
|
+
});
|
|
907
1272
|
var ProvisionRegistry = class {
|
|
908
1273
|
providers = /* @__PURE__ */ new Map();
|
|
909
1274
|
register(provider) {
|
|
@@ -920,16 +1285,20 @@ var awsStsProvider = {
|
|
|
920
1285
|
name: "aws-sts",
|
|
921
1286
|
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
922
1287
|
provision(configRaw) {
|
|
923
|
-
let
|
|
1288
|
+
let raw;
|
|
924
1289
|
try {
|
|
925
|
-
|
|
1290
|
+
raw = JSON.parse(configRaw);
|
|
926
1291
|
} catch {
|
|
927
1292
|
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
928
1293
|
}
|
|
1294
|
+
const parsed = AwsStsConfigSchema.safeParse(raw);
|
|
1295
|
+
if (!parsed.success) {
|
|
1296
|
+
throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
|
|
1297
|
+
}
|
|
1298
|
+
const config = parsed.data;
|
|
929
1299
|
const roleArn = config.roleArn;
|
|
930
1300
|
const sessionName = config.sessionName || "q-ring-agent";
|
|
931
1301
|
const duration = config.durationSeconds || 3600;
|
|
932
|
-
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
933
1302
|
try {
|
|
934
1303
|
const output = execFileSync("aws", [
|
|
935
1304
|
"sts",
|
|
@@ -943,8 +1312,8 @@ var awsStsProvider = {
|
|
|
943
1312
|
"--output",
|
|
944
1313
|
"json"
|
|
945
1314
|
], { encoding: "utf8" });
|
|
946
|
-
const
|
|
947
|
-
const creds =
|
|
1315
|
+
const parsed2 = JSON.parse(output);
|
|
1316
|
+
const creds = parsed2.Credentials;
|
|
948
1317
|
const value = JSON.stringify({
|
|
949
1318
|
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
950
1319
|
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
@@ -963,18 +1332,23 @@ var httpProvider = {
|
|
|
963
1332
|
name: "http",
|
|
964
1333
|
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
965
1334
|
provision(configRaw) {
|
|
966
|
-
let
|
|
1335
|
+
let raw;
|
|
967
1336
|
try {
|
|
968
|
-
|
|
1337
|
+
raw = JSON.parse(configRaw);
|
|
969
1338
|
} catch {
|
|
970
1339
|
throw new Error("http provider requires valid JSON config");
|
|
971
1340
|
}
|
|
1341
|
+
const parsed = HttpJitConfigSchema.safeParse(raw);
|
|
1342
|
+
if (!parsed.success) {
|
|
1343
|
+
throw new Error(`http provider invalid config: ${parsed.error.message}`);
|
|
1344
|
+
}
|
|
1345
|
+
const config = parsed.data;
|
|
972
1346
|
const url = config.url;
|
|
973
1347
|
const method = config.method || "POST";
|
|
974
1348
|
const valuePath = config.valuePath || "token";
|
|
975
1349
|
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
976
1350
|
if (!url) throw new Error("http provider requires url in config");
|
|
977
|
-
const ssrfBlock =
|
|
1351
|
+
const ssrfBlock = checkJitHttpProvisionUrl(url);
|
|
978
1352
|
if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
|
|
979
1353
|
const headers = {
|
|
980
1354
|
"User-Agent": "q-ring-jit/1.0",
|
|
@@ -1008,8 +1382,8 @@ req.end();
|
|
|
1008
1382
|
if (result.status !== 0) {
|
|
1009
1383
|
throw new Error(result.stderr || "HTTP request failed");
|
|
1010
1384
|
}
|
|
1011
|
-
const
|
|
1012
|
-
let val =
|
|
1385
|
+
const parsed2 = JSON.parse(result.stdout);
|
|
1386
|
+
let val = parsed2;
|
|
1013
1387
|
for (const key of valuePath.split(".")) {
|
|
1014
1388
|
val = val[key];
|
|
1015
1389
|
}
|
|
@@ -1026,105 +1400,33 @@ var registry = new ProvisionRegistry();
|
|
|
1026
1400
|
registry.register(awsStsProvider);
|
|
1027
1401
|
registry.register(httpProvider);
|
|
1028
1402
|
|
|
1029
|
-
// src/core/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
return {
|
|
1053
|
-
allowed: false,
|
|
1054
|
-
reason: `Tool "${toolName}" is not in the allowlist`,
|
|
1055
|
-
policySource: ".q-ring.json policy.mcp.allowTools"
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
return { allowed: true, policySource: ".q-ring.json" };
|
|
1059
|
-
}
|
|
1060
|
-
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1061
|
-
const policy = loadPolicy(projectPath);
|
|
1062
|
-
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1063
|
-
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1064
|
-
return {
|
|
1065
|
-
allowed: false,
|
|
1066
|
-
reason: `Key "${key}" is denied by project policy`,
|
|
1067
|
-
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1071
|
-
return {
|
|
1072
|
-
allowed: false,
|
|
1073
|
-
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1074
|
-
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
if (tags && policy.mcp.deniedTags) {
|
|
1078
|
-
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1079
|
-
if (blocked) {
|
|
1080
|
-
return {
|
|
1081
|
-
allowed: false,
|
|
1082
|
-
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1083
|
-
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
return { allowed: true, policySource: ".q-ring.json" };
|
|
1088
|
-
}
|
|
1089
|
-
function checkExecPolicy(command, projectPath) {
|
|
1090
|
-
const policy = loadPolicy(projectPath);
|
|
1091
|
-
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1092
|
-
if (policy.exec.denyCommands) {
|
|
1093
|
-
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1094
|
-
if (denied) {
|
|
1095
|
-
return {
|
|
1096
|
-
allowed: false,
|
|
1097
|
-
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1098
|
-
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
if (policy.exec.allowCommands) {
|
|
1103
|
-
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1104
|
-
if (!allowed) {
|
|
1105
|
-
return {
|
|
1106
|
-
allowed: false,
|
|
1107
|
-
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1108
|
-
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1109
|
-
};
|
|
1403
|
+
// src/core/keyring.ts
|
|
1404
|
+
function withJitEnvelopeLock(service, key, fn) {
|
|
1405
|
+
const dir = join7(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1406
|
+
mkdirSync5(dir, { recursive: true });
|
|
1407
|
+
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1408
|
+
const lockPath = join7(dir, `${safe}.lock`);
|
|
1409
|
+
const deadline = Date.now() + 8e3;
|
|
1410
|
+
while (Date.now() < deadline) {
|
|
1411
|
+
try {
|
|
1412
|
+
writeFileSync4(lockPath, `${process.pid}
|
|
1413
|
+
`, { flag: "wx", mode: 384 });
|
|
1414
|
+
try {
|
|
1415
|
+
return fn();
|
|
1416
|
+
} finally {
|
|
1417
|
+
try {
|
|
1418
|
+
unlinkSync(lockPath);
|
|
1419
|
+
} catch {
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
} catch {
|
|
1423
|
+
const start = Date.now();
|
|
1424
|
+
while (Date.now() - start < 15) {
|
|
1425
|
+
}
|
|
1110
1426
|
}
|
|
1111
1427
|
}
|
|
1112
|
-
|
|
1428
|
+
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1113
1429
|
}
|
|
1114
|
-
function getExecMaxRuntime(projectPath) {
|
|
1115
|
-
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1116
|
-
}
|
|
1117
|
-
function getPolicySummary(projectPath) {
|
|
1118
|
-
const policy = loadPolicy(projectPath);
|
|
1119
|
-
return {
|
|
1120
|
-
hasMcpPolicy: !!policy.mcp,
|
|
1121
|
-
hasExecPolicy: !!policy.exec,
|
|
1122
|
-
hasSecretPolicy: !!policy.secrets,
|
|
1123
|
-
details: policy
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// src/core/keyring.ts
|
|
1128
1430
|
function readEnvelope(service, key) {
|
|
1129
1431
|
const entry = new Entry(service, key);
|
|
1130
1432
|
const raw = entry.getPassword();
|
|
@@ -1143,7 +1445,7 @@ function resolveEnv(opts) {
|
|
|
1143
1445
|
}
|
|
1144
1446
|
function resolveTemplates(value, opts, seen) {
|
|
1145
1447
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1146
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1448
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1147
1449
|
const refKey = refKeyRaw.trim();
|
|
1148
1450
|
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1149
1451
|
if (refValue === null) {
|
|
@@ -1154,7 +1456,7 @@ function resolveTemplates(value, opts, seen) {
|
|
|
1154
1456
|
}
|
|
1155
1457
|
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1156
1458
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1157
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1459
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1158
1460
|
const refKey = refKeyRaw.trim();
|
|
1159
1461
|
if (seen.has(refKey)) {
|
|
1160
1462
|
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
@@ -1224,11 +1526,27 @@ function getSecret(key, opts = {}) {
|
|
|
1224
1526
|
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1225
1527
|
}
|
|
1226
1528
|
if (isExpired) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1529
|
+
const jitConfigSource = value;
|
|
1530
|
+
withJitEnvelopeLock(service, key, () => {
|
|
1531
|
+
const latest = readEnvelope(service, key);
|
|
1532
|
+
if (!latest?.meta.jitProvider) return;
|
|
1533
|
+
let innerExpired = true;
|
|
1534
|
+
if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
|
|
1535
|
+
innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1536
|
+
}
|
|
1537
|
+
if (!innerExpired) {
|
|
1538
|
+
envelope.states = latest.states;
|
|
1539
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const result = provider.provision(jitConfigSource);
|
|
1543
|
+
latest.states = latest.states ?? {};
|
|
1544
|
+
latest.states["jit"] = result.value;
|
|
1545
|
+
latest.meta.jitExpiresAt = result.expiresAt;
|
|
1546
|
+
writeEnvelope(service, key, latest);
|
|
1547
|
+
envelope.states = latest.states;
|
|
1548
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1549
|
+
});
|
|
1232
1550
|
}
|
|
1233
1551
|
value = envelope.states["jit"];
|
|
1234
1552
|
}
|
|
@@ -1270,6 +1588,20 @@ function setSecret(key, value, opts = {}) {
|
|
|
1270
1588
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1271
1589
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1272
1590
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1591
|
+
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
1592
|
+
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
1593
|
+
const life = checkSecretLifecyclePolicy(
|
|
1594
|
+
{
|
|
1595
|
+
tags: mergedTags,
|
|
1596
|
+
ttlSeconds: ttlForPolicy,
|
|
1597
|
+
rotationFormat: rotFmt,
|
|
1598
|
+
requiresApproval: !!reqApp
|
|
1599
|
+
},
|
|
1600
|
+
opts.projectPath
|
|
1601
|
+
);
|
|
1602
|
+
if (!life.allowed) {
|
|
1603
|
+
throw new Error(life.reason ?? "Secret lifecycle policy denied");
|
|
1604
|
+
}
|
|
1273
1605
|
if (opts.states) {
|
|
1274
1606
|
envelope = createEnvelope("", {
|
|
1275
1607
|
states: opts.states,
|
|
@@ -1586,7 +1918,122 @@ function tunnelList() {
|
|
|
1586
1918
|
return result;
|
|
1587
1919
|
}
|
|
1588
1920
|
|
|
1921
|
+
// src/core/memory.ts
|
|
1922
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1923
|
+
import { join as join8 } from "path";
|
|
1924
|
+
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1925
|
+
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1926
|
+
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
1927
|
+
var MEMORY_FILE = "agent-memory.enc";
|
|
1928
|
+
var KEYRING_SERVICE = "qring-memory-key";
|
|
1929
|
+
var KEYRING_ACCOUNT = "encryption-key";
|
|
1930
|
+
function getMemoryDir() {
|
|
1931
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1932
|
+
if (!existsSync6(dir)) {
|
|
1933
|
+
mkdirSync6(dir, { recursive: true });
|
|
1934
|
+
}
|
|
1935
|
+
return dir;
|
|
1936
|
+
}
|
|
1937
|
+
function getMemoryPath() {
|
|
1938
|
+
return join8(getMemoryDir(), MEMORY_FILE);
|
|
1939
|
+
}
|
|
1940
|
+
function deriveLegacyKey() {
|
|
1941
|
+
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1942
|
+
return createHash3("sha256").update(fingerprint).digest();
|
|
1943
|
+
}
|
|
1944
|
+
function getOrCreateKey() {
|
|
1945
|
+
try {
|
|
1946
|
+
const entry = new Entry2(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
1947
|
+
const stored = entry.getPassword();
|
|
1948
|
+
if (stored) return Buffer.from(stored, "base64");
|
|
1949
|
+
const key = randomBytes3(32);
|
|
1950
|
+
entry.setPassword(key.toString("base64"));
|
|
1951
|
+
return key;
|
|
1952
|
+
} catch {
|
|
1953
|
+
console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
|
|
1954
|
+
return deriveLegacyKey();
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function encryptWith(data, key) {
|
|
1958
|
+
const iv = randomBytes3(12);
|
|
1959
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
1960
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
1961
|
+
const tag = cipher.getAuthTag();
|
|
1962
|
+
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
1963
|
+
}
|
|
1964
|
+
function decryptWith(blob, key) {
|
|
1965
|
+
const parts = blob.split(":");
|
|
1966
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
1967
|
+
const iv = Buffer.from(parts[0], "base64");
|
|
1968
|
+
const tag = Buffer.from(parts[1], "base64");
|
|
1969
|
+
const encrypted = Buffer.from(parts[2], "base64");
|
|
1970
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
1971
|
+
decipher.setAuthTag(tag);
|
|
1972
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
1973
|
+
}
|
|
1974
|
+
function encrypt(data) {
|
|
1975
|
+
return encryptWith(data, getOrCreateKey());
|
|
1976
|
+
}
|
|
1977
|
+
function decrypt(blob) {
|
|
1978
|
+
const key = getOrCreateKey();
|
|
1979
|
+
try {
|
|
1980
|
+
return decryptWith(blob, key);
|
|
1981
|
+
} catch {
|
|
1982
|
+
const legacy = deriveLegacyKey();
|
|
1983
|
+
const plain = decryptWith(blob, legacy);
|
|
1984
|
+
writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
|
|
1985
|
+
return plain;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function loadStore() {
|
|
1989
|
+
const path = getMemoryPath();
|
|
1990
|
+
if (!existsSync6(path)) {
|
|
1991
|
+
return { entries: {} };
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
const raw = readFileSync7(path, "utf8");
|
|
1995
|
+
const decrypted = decrypt(raw);
|
|
1996
|
+
return JSON.parse(decrypted);
|
|
1997
|
+
} catch {
|
|
1998
|
+
return { entries: {} };
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function saveStore(store) {
|
|
2002
|
+
const json = JSON.stringify(store);
|
|
2003
|
+
const encrypted = encrypt(json);
|
|
2004
|
+
writeFileSync5(getMemoryPath(), encrypted, "utf8");
|
|
2005
|
+
}
|
|
2006
|
+
function remember(key, value) {
|
|
2007
|
+
const store = loadStore();
|
|
2008
|
+
store.entries[key] = {
|
|
2009
|
+
value,
|
|
2010
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2011
|
+
};
|
|
2012
|
+
saveStore(store);
|
|
2013
|
+
}
|
|
2014
|
+
function recall(key) {
|
|
2015
|
+
const store = loadStore();
|
|
2016
|
+
return store.entries[key]?.value ?? null;
|
|
2017
|
+
}
|
|
2018
|
+
function listMemory() {
|
|
2019
|
+
const store = loadStore();
|
|
2020
|
+
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
2021
|
+
key,
|
|
2022
|
+
updatedAt: entry.updatedAt
|
|
2023
|
+
}));
|
|
2024
|
+
}
|
|
2025
|
+
function forget(key) {
|
|
2026
|
+
const store = loadStore();
|
|
2027
|
+
if (key in store.entries) {
|
|
2028
|
+
delete store.entries[key];
|
|
2029
|
+
saveStore(store);
|
|
2030
|
+
return true;
|
|
2031
|
+
}
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1589
2035
|
export {
|
|
2036
|
+
PACKAGE_VERSION,
|
|
1590
2037
|
checkDecay,
|
|
1591
2038
|
readProjectConfig,
|
|
1592
2039
|
collapseEnvironment,
|
|
@@ -1596,12 +2043,13 @@ export {
|
|
|
1596
2043
|
exportAudit,
|
|
1597
2044
|
detectAnomalies,
|
|
1598
2045
|
listEntanglements,
|
|
1599
|
-
|
|
2046
|
+
httpRequest,
|
|
1600
2047
|
checkSSRF,
|
|
1601
2048
|
registerHook,
|
|
1602
2049
|
removeHook,
|
|
1603
2050
|
listHooks,
|
|
1604
2051
|
fireHooks,
|
|
2052
|
+
listApprovals,
|
|
1605
2053
|
registry,
|
|
1606
2054
|
checkToolPolicy,
|
|
1607
2055
|
checkKeyReadPolicy,
|
|
@@ -1620,6 +2068,10 @@ export {
|
|
|
1620
2068
|
tunnelCreate,
|
|
1621
2069
|
tunnelRead,
|
|
1622
2070
|
tunnelDestroy,
|
|
1623
|
-
tunnelList
|
|
2071
|
+
tunnelList,
|
|
2072
|
+
remember,
|
|
2073
|
+
recall,
|
|
2074
|
+
listMemory,
|
|
2075
|
+
forget
|
|
1624
2076
|
};
|
|
1625
|
-
//# sourceMappingURL=chunk-
|
|
2077
|
+
//# sourceMappingURL=chunk-MQM4DLPI.js.map
|