@i4ctime/q-ring 0.11.5 → 0.12.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 +55 -51
- package/dist/{chunk-A4RZVP3P.js → chunk-5TQNDJT3.js} +160 -90
- package/dist/chunk-5TQNDJT3.js.map +1 -0
- package/dist/{chunk-MQM4DLPI.js → chunk-YWDKAZWS.js} +165 -90
- package/dist/chunk-YWDKAZWS.js.map +1 -0
- package/dist/dashboard-6XKC5BL4.js +630 -0
- package/dist/dashboard-6XKC5BL4.js.map +1 -0
- package/dist/dashboard-HHJY73JX.js +630 -0
- package/dist/dashboard-HHJY73JX.js.map +1 -0
- package/dist/index.js +45 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +516 -166
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -4
- package/dist/chunk-A4RZVP3P.js.map +0 -1
- package/dist/chunk-MQM4DLPI.js.map +0 -1
- package/dist/dashboard-FSCJDLJX.js +0 -1313
- package/dist/dashboard-FSCJDLJX.js.map +0 -1
- package/dist/dashboard-OXUD62RV.js +0 -1313
- package/dist/dashboard-OXUD62RV.js.map +0 -1
|
@@ -555,87 +555,11 @@ import { randomUUID } from "crypto";
|
|
|
555
555
|
// src/utils/http-request.ts
|
|
556
556
|
import { request as httpsRequest } from "https";
|
|
557
557
|
import { request as httpRequestPlain } from "http";
|
|
558
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
559
|
-
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
560
|
-
function httpRequest(opts) {
|
|
561
|
-
const {
|
|
562
|
-
url,
|
|
563
|
-
method = "GET",
|
|
564
|
-
headers = {},
|
|
565
|
-
body,
|
|
566
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
567
|
-
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
568
|
-
} = opts;
|
|
569
|
-
return new Promise((resolve, reject) => {
|
|
570
|
-
const parsed = new URL(url);
|
|
571
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
572
|
-
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
576
|
-
const reqHeaders = { ...headers };
|
|
577
|
-
if (body && !reqHeaders["Content-Length"]) {
|
|
578
|
-
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
579
|
-
}
|
|
580
|
-
const req = reqFn(
|
|
581
|
-
url,
|
|
582
|
-
{ method, headers: reqHeaders, timeout: timeoutMs },
|
|
583
|
-
(res) => {
|
|
584
|
-
const chunks = [];
|
|
585
|
-
let totalBytes = 0;
|
|
586
|
-
let truncated = false;
|
|
587
|
-
res.on("data", (chunk) => {
|
|
588
|
-
totalBytes += chunk.length;
|
|
589
|
-
if (totalBytes > maxResponseBytes) {
|
|
590
|
-
truncated = true;
|
|
591
|
-
res.destroy();
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
chunks.push(chunk);
|
|
595
|
-
});
|
|
596
|
-
let settled = false;
|
|
597
|
-
const settle = (result) => {
|
|
598
|
-
if (!settled) {
|
|
599
|
-
settled = true;
|
|
600
|
-
resolve(result);
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
const fail = (err) => {
|
|
604
|
-
if (!settled) {
|
|
605
|
-
settled = true;
|
|
606
|
-
reject(err);
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
610
|
-
res.on("end", () => {
|
|
611
|
-
settle({
|
|
612
|
-
statusCode: res.statusCode ?? 0,
|
|
613
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
614
|
-
truncated
|
|
615
|
-
});
|
|
616
|
-
});
|
|
617
|
-
res.on("close", () => {
|
|
618
|
-
settle({
|
|
619
|
-
statusCode: res.statusCode ?? 0,
|
|
620
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
621
|
-
truncated
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
627
|
-
req.on("timeout", () => {
|
|
628
|
-
req.destroy();
|
|
629
|
-
reject(new Error("Request timed out"));
|
|
630
|
-
});
|
|
631
|
-
if (body) req.write(body);
|
|
632
|
-
req.end();
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
558
|
|
|
636
559
|
// src/core/ssrf.ts
|
|
637
560
|
import { lookup } from "dns/promises";
|
|
638
561
|
import * as dns from "dns";
|
|
562
|
+
import { lookup as dnsLookup } from "dns";
|
|
639
563
|
import { isIPv4, isIPv6 } from "net";
|
|
640
564
|
function lookupAddressesSync(hostname2) {
|
|
641
565
|
const lookupSync2 = dns.lookupSync;
|
|
@@ -682,6 +606,36 @@ async function checkSSRF(url) {
|
|
|
682
606
|
}
|
|
683
607
|
return null;
|
|
684
608
|
}
|
|
609
|
+
function guardedLookup(hostname2, options, callback) {
|
|
610
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") {
|
|
611
|
+
dnsLookup(
|
|
612
|
+
hostname2,
|
|
613
|
+
options,
|
|
614
|
+
callback
|
|
615
|
+
);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
dnsLookup(
|
|
619
|
+
hostname2,
|
|
620
|
+
options,
|
|
621
|
+
(err, address, family) => {
|
|
622
|
+
if (err) return callback(err, address, family);
|
|
623
|
+
const list = Array.isArray(address) ? address : [{ address, family: family ?? 0 }];
|
|
624
|
+
for (const a of list) {
|
|
625
|
+
if (isPrivateIP(a.address)) {
|
|
626
|
+
const blocked = Object.assign(
|
|
627
|
+
new Error(
|
|
628
|
+
`Blocked: "${hostname2}" resolved to private address ${a.address} at connect time.`
|
|
629
|
+
),
|
|
630
|
+
{ code: "EQRINGSSRF" }
|
|
631
|
+
);
|
|
632
|
+
return callback(blocked, address, family);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
callback(null, address, family);
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
685
639
|
function checkJitHttpProvisionUrl(url) {
|
|
686
640
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
687
641
|
try {
|
|
@@ -719,6 +673,91 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
719
673
|
return null;
|
|
720
674
|
}
|
|
721
675
|
|
|
676
|
+
// src/utils/http-request.ts
|
|
677
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
678
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
679
|
+
function httpRequest(opts) {
|
|
680
|
+
const {
|
|
681
|
+
url,
|
|
682
|
+
method = "GET",
|
|
683
|
+
headers = {},
|
|
684
|
+
body,
|
|
685
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
686
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
687
|
+
} = opts;
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const parsed = new URL(url);
|
|
690
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
691
|
+
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
695
|
+
const reqHeaders = { ...headers };
|
|
696
|
+
if (body && !reqHeaders["Content-Length"]) {
|
|
697
|
+
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
698
|
+
}
|
|
699
|
+
const req = reqFn(
|
|
700
|
+
url,
|
|
701
|
+
{
|
|
702
|
+
method,
|
|
703
|
+
headers: reqHeaders,
|
|
704
|
+
timeout: timeoutMs,
|
|
705
|
+
// Re-validate the resolved IP at connect time (DNS-rebinding guard).
|
|
706
|
+
lookup: guardedLookup
|
|
707
|
+
},
|
|
708
|
+
(res) => {
|
|
709
|
+
const chunks = [];
|
|
710
|
+
let totalBytes = 0;
|
|
711
|
+
let truncated = false;
|
|
712
|
+
res.on("data", (chunk) => {
|
|
713
|
+
totalBytes += chunk.length;
|
|
714
|
+
if (totalBytes > maxResponseBytes) {
|
|
715
|
+
truncated = true;
|
|
716
|
+
res.destroy();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
chunks.push(chunk);
|
|
720
|
+
});
|
|
721
|
+
let settled = false;
|
|
722
|
+
const settle = (result) => {
|
|
723
|
+
if (!settled) {
|
|
724
|
+
settled = true;
|
|
725
|
+
resolve(result);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
const fail = (err) => {
|
|
729
|
+
if (!settled) {
|
|
730
|
+
settled = true;
|
|
731
|
+
reject(err);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
735
|
+
res.on("end", () => {
|
|
736
|
+
settle({
|
|
737
|
+
statusCode: res.statusCode ?? 0,
|
|
738
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
739
|
+
truncated
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
res.on("close", () => {
|
|
743
|
+
settle({
|
|
744
|
+
statusCode: res.statusCode ?? 0,
|
|
745
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
746
|
+
truncated
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
752
|
+
req.on("timeout", () => {
|
|
753
|
+
req.destroy();
|
|
754
|
+
reject(new Error("Request timed out"));
|
|
755
|
+
});
|
|
756
|
+
if (body) req.write(body);
|
|
757
|
+
req.end();
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
722
761
|
// src/core/hooks.ts
|
|
723
762
|
function getRegistryPath2() {
|
|
724
763
|
const dir = join5(homedir3(), ".config", "q-ring");
|
|
@@ -1065,15 +1104,33 @@ function listApprovals() {
|
|
|
1065
1104
|
}
|
|
1066
1105
|
|
|
1067
1106
|
// src/core/policy.ts
|
|
1107
|
+
import { statSync as statSync2 } from "fs";
|
|
1108
|
+
import { join as join7 } from "path";
|
|
1068
1109
|
var cachedPolicy = null;
|
|
1110
|
+
var policyRoot = null;
|
|
1111
|
+
function setPolicyRoot(root) {
|
|
1112
|
+
policyRoot = root;
|
|
1113
|
+
cachedPolicy = null;
|
|
1114
|
+
}
|
|
1115
|
+
function resolvePolicyPath(projectPath) {
|
|
1116
|
+
return policyRoot ?? projectPath ?? process.cwd();
|
|
1117
|
+
}
|
|
1118
|
+
function configMtime(pp) {
|
|
1119
|
+
try {
|
|
1120
|
+
return statSync2(join7(pp, ".q-ring.json")).mtimeMs;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return 0;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1069
1125
|
function loadPolicy(projectPath) {
|
|
1070
|
-
const pp = projectPath
|
|
1071
|
-
|
|
1126
|
+
const pp = resolvePolicyPath(projectPath);
|
|
1127
|
+
const mtimeMs = configMtime(pp);
|
|
1128
|
+
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1072
1129
|
return cachedPolicy.policy;
|
|
1073
1130
|
}
|
|
1074
1131
|
const config = readProjectConfig(pp);
|
|
1075
1132
|
const policy = config?.policy ?? {};
|
|
1076
|
-
cachedPolicy = { path: pp, policy };
|
|
1133
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1077
1134
|
return policy;
|
|
1078
1135
|
}
|
|
1079
1136
|
function checkToolPolicy(toolName, projectPath) {
|
|
@@ -1161,7 +1218,13 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1161
1218
|
const policy = loadPolicy(projectPath);
|
|
1162
1219
|
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1163
1220
|
if (policy.exec.denyCommands) {
|
|
1164
|
-
const denied = policy.exec.denyCommands.find((d) =>
|
|
1221
|
+
const denied = policy.exec.denyCommands.find((d) => {
|
|
1222
|
+
const pattern = new RegExp(
|
|
1223
|
+
`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`,
|
|
1224
|
+
"i"
|
|
1225
|
+
);
|
|
1226
|
+
return pattern.test(command);
|
|
1227
|
+
});
|
|
1165
1228
|
if (denied) {
|
|
1166
1229
|
return {
|
|
1167
1230
|
allowed: false,
|
|
@@ -1171,7 +1234,8 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1171
1234
|
}
|
|
1172
1235
|
}
|
|
1173
1236
|
if (policy.exec.allowCommands) {
|
|
1174
|
-
const
|
|
1237
|
+
const normalized = command.trimStart();
|
|
1238
|
+
const allowed = policy.exec.allowCommands.some((a) => normalized.startsWith(a));
|
|
1175
1239
|
if (!allowed) {
|
|
1176
1240
|
return {
|
|
1177
1241
|
allowed: false,
|
|
@@ -1198,7 +1262,7 @@ function getPolicySummary(projectPath) {
|
|
|
1198
1262
|
// src/core/keyring.ts
|
|
1199
1263
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1200
1264
|
import { homedir as homedir5 } from "os";
|
|
1201
|
-
import { join as
|
|
1265
|
+
import { join as join8 } from "path";
|
|
1202
1266
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1203
1267
|
|
|
1204
1268
|
// src/utils/hash.ts
|
|
@@ -1402,10 +1466,10 @@ registry.register(httpProvider);
|
|
|
1402
1466
|
|
|
1403
1467
|
// src/core/keyring.ts
|
|
1404
1468
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1405
|
-
const dir =
|
|
1469
|
+
const dir = join8(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1406
1470
|
mkdirSync5(dir, { recursive: true });
|
|
1407
1471
|
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1408
|
-
const lockPath =
|
|
1472
|
+
const lockPath = join8(dir, `${safe}.lock`);
|
|
1409
1473
|
const deadline = Date.now() + 8e3;
|
|
1410
1474
|
while (Date.now() < deadline) {
|
|
1411
1475
|
try {
|
|
@@ -1789,6 +1853,16 @@ function exportSecrets(opts = {}) {
|
|
|
1789
1853
|
if (entry.envelope) {
|
|
1790
1854
|
const decay = checkDecay(entry.envelope);
|
|
1791
1855
|
if (decay.isExpired) continue;
|
|
1856
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
1857
|
+
logAudit({
|
|
1858
|
+
action: "read",
|
|
1859
|
+
key: entry.key,
|
|
1860
|
+
scope: entry.scope,
|
|
1861
|
+
source,
|
|
1862
|
+
detail: "blocked: requires user approval (export)"
|
|
1863
|
+
});
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1792
1866
|
const value = collapseValue(entry.envelope, env);
|
|
1793
1867
|
if (value !== null) {
|
|
1794
1868
|
rawValues.set(entry.key, value);
|
|
@@ -1920,7 +1994,7 @@ function tunnelList() {
|
|
|
1920
1994
|
|
|
1921
1995
|
// src/core/memory.ts
|
|
1922
1996
|
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1923
|
-
import { join as
|
|
1997
|
+
import { join as join9 } from "path";
|
|
1924
1998
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1925
1999
|
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1926
2000
|
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
@@ -1928,14 +2002,14 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
1928
2002
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
1929
2003
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
1930
2004
|
function getMemoryDir() {
|
|
1931
|
-
const dir =
|
|
2005
|
+
const dir = join9(homedir6(), ".config", "q-ring");
|
|
1932
2006
|
if (!existsSync6(dir)) {
|
|
1933
2007
|
mkdirSync6(dir, { recursive: true });
|
|
1934
2008
|
}
|
|
1935
2009
|
return dir;
|
|
1936
2010
|
}
|
|
1937
2011
|
function getMemoryPath() {
|
|
1938
|
-
return
|
|
2012
|
+
return join9(getMemoryDir(), MEMORY_FILE);
|
|
1939
2013
|
}
|
|
1940
2014
|
function deriveLegacyKey() {
|
|
1941
2015
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
@@ -2043,14 +2117,15 @@ export {
|
|
|
2043
2117
|
exportAudit,
|
|
2044
2118
|
detectAnomalies,
|
|
2045
2119
|
listEntanglements,
|
|
2046
|
-
httpRequest,
|
|
2047
2120
|
checkSSRF,
|
|
2121
|
+
httpRequest,
|
|
2048
2122
|
registerHook,
|
|
2049
2123
|
removeHook,
|
|
2050
2124
|
listHooks,
|
|
2051
2125
|
fireHooks,
|
|
2052
2126
|
listApprovals,
|
|
2053
2127
|
registry,
|
|
2128
|
+
setPolicyRoot,
|
|
2054
2129
|
checkToolPolicy,
|
|
2055
2130
|
checkKeyReadPolicy,
|
|
2056
2131
|
checkExecPolicy,
|
|
@@ -2074,4 +2149,4 @@ export {
|
|
|
2074
2149
|
listMemory,
|
|
2075
2150
|
forget
|
|
2076
2151
|
};
|
|
2077
|
-
//# sourceMappingURL=chunk-
|
|
2152
|
+
//# sourceMappingURL=chunk-YWDKAZWS.js.map
|