@i4ctime/q-ring 0.11.7 → 0.13.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 +45 -10
- package/dist/{chunk-MQM4DLPI.js → chunk-CWV3WTPF.js} +168 -95
- package/dist/chunk-CWV3WTPF.js.map +1 -0
- package/dist/{chunk-A4RZVP3P.js → chunk-SRESNRML.js} +163 -95
- package/dist/chunk-SRESNRML.js.map +1 -0
- package/dist/dashboard-II2ILNXA.js +658 -0
- package/dist/dashboard-II2ILNXA.js.map +1 -0
- package/dist/dashboard-RLMROWIU.js +658 -0
- package/dist/dashboard-RLMROWIU.js.map +1 -0
- package/dist/index.js +712 -145
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +49 -31
- package/dist/mcp.js.map +1 -1
- package/package.json +29 -14
- 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
|
@@ -147,7 +147,7 @@ function checkDecay(envelope) {
|
|
|
147
147
|
const remaining = expires - now;
|
|
148
148
|
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
149
149
|
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
150
|
-
let timeRemaining =
|
|
150
|
+
let timeRemaining = "expired";
|
|
151
151
|
if (remaining > 0) {
|
|
152
152
|
const days = Math.floor(remaining / 864e5);
|
|
153
153
|
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
@@ -155,8 +155,6 @@ function checkDecay(envelope) {
|
|
|
155
155
|
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
156
156
|
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
157
157
|
else timeRemaining = `${minutes}m`;
|
|
158
|
-
} else {
|
|
159
|
-
timeRemaining = "expired";
|
|
160
158
|
}
|
|
161
159
|
return {
|
|
162
160
|
isExpired: remaining <= 0,
|
|
@@ -555,87 +553,11 @@ import { randomUUID } from "crypto";
|
|
|
555
553
|
// src/utils/http-request.ts
|
|
556
554
|
import { request as httpsRequest } from "https";
|
|
557
555
|
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
556
|
|
|
636
557
|
// src/core/ssrf.ts
|
|
637
558
|
import { lookup } from "dns/promises";
|
|
638
559
|
import * as dns from "dns";
|
|
560
|
+
import { lookup as dnsLookup } from "dns";
|
|
639
561
|
import { isIPv4, isIPv6 } from "net";
|
|
640
562
|
function lookupAddressesSync(hostname2) {
|
|
641
563
|
const lookupSync2 = dns.lookupSync;
|
|
@@ -682,6 +604,36 @@ async function checkSSRF(url) {
|
|
|
682
604
|
}
|
|
683
605
|
return null;
|
|
684
606
|
}
|
|
607
|
+
function guardedLookup(hostname2, options, callback) {
|
|
608
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") {
|
|
609
|
+
dnsLookup(
|
|
610
|
+
hostname2,
|
|
611
|
+
options,
|
|
612
|
+
callback
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
dnsLookup(
|
|
617
|
+
hostname2,
|
|
618
|
+
options,
|
|
619
|
+
(err, address, family) => {
|
|
620
|
+
if (err) return callback(err, address, family);
|
|
621
|
+
const list = Array.isArray(address) ? address : [{ address, family: family ?? 0 }];
|
|
622
|
+
for (const a of list) {
|
|
623
|
+
if (isPrivateIP(a.address)) {
|
|
624
|
+
const blocked = Object.assign(
|
|
625
|
+
new Error(
|
|
626
|
+
`Blocked: "${hostname2}" resolved to private address ${a.address} at connect time.`
|
|
627
|
+
),
|
|
628
|
+
{ code: "EQRINGSSRF" }
|
|
629
|
+
);
|
|
630
|
+
return callback(blocked, address, family);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
callback(null, address, family);
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
685
637
|
function checkJitHttpProvisionUrl(url) {
|
|
686
638
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
687
639
|
try {
|
|
@@ -719,6 +671,91 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
719
671
|
return null;
|
|
720
672
|
}
|
|
721
673
|
|
|
674
|
+
// src/utils/http-request.ts
|
|
675
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
676
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
677
|
+
function httpRequest(opts) {
|
|
678
|
+
const {
|
|
679
|
+
url,
|
|
680
|
+
method = "GET",
|
|
681
|
+
headers = {},
|
|
682
|
+
body,
|
|
683
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
684
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
685
|
+
} = opts;
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
const parsed = new URL(url);
|
|
688
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
689
|
+
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
693
|
+
const reqHeaders = { ...headers };
|
|
694
|
+
if (body && !reqHeaders["Content-Length"]) {
|
|
695
|
+
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
696
|
+
}
|
|
697
|
+
const req = reqFn(
|
|
698
|
+
url,
|
|
699
|
+
{
|
|
700
|
+
method,
|
|
701
|
+
headers: reqHeaders,
|
|
702
|
+
timeout: timeoutMs,
|
|
703
|
+
// Re-validate the resolved IP at connect time (DNS-rebinding guard).
|
|
704
|
+
lookup: guardedLookup
|
|
705
|
+
},
|
|
706
|
+
(res) => {
|
|
707
|
+
const chunks = [];
|
|
708
|
+
let totalBytes = 0;
|
|
709
|
+
let truncated = false;
|
|
710
|
+
res.on("data", (chunk) => {
|
|
711
|
+
totalBytes += chunk.length;
|
|
712
|
+
if (totalBytes > maxResponseBytes) {
|
|
713
|
+
truncated = true;
|
|
714
|
+
res.destroy();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
chunks.push(chunk);
|
|
718
|
+
});
|
|
719
|
+
let settled = false;
|
|
720
|
+
const settle = (result) => {
|
|
721
|
+
if (!settled) {
|
|
722
|
+
settled = true;
|
|
723
|
+
resolve(result);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const fail = (err) => {
|
|
727
|
+
if (!settled) {
|
|
728
|
+
settled = true;
|
|
729
|
+
reject(err);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
733
|
+
res.on("end", () => {
|
|
734
|
+
settle({
|
|
735
|
+
statusCode: res.statusCode ?? 0,
|
|
736
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
737
|
+
truncated
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
res.on("close", () => {
|
|
741
|
+
settle({
|
|
742
|
+
statusCode: res.statusCode ?? 0,
|
|
743
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
744
|
+
truncated
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
750
|
+
req.on("timeout", () => {
|
|
751
|
+
req.destroy();
|
|
752
|
+
reject(new Error("Request timed out"));
|
|
753
|
+
});
|
|
754
|
+
if (body) req.write(body);
|
|
755
|
+
req.end();
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
722
759
|
// src/core/hooks.ts
|
|
723
760
|
function getRegistryPath2() {
|
|
724
761
|
const dir = join5(homedir3(), ".config", "q-ring");
|
|
@@ -1128,15 +1165,29 @@ function listApprovals() {
|
|
|
1128
1165
|
}
|
|
1129
1166
|
|
|
1130
1167
|
// src/core/policy.ts
|
|
1168
|
+
import { statSync as statSync2 } from "fs";
|
|
1169
|
+
import { join as join7 } from "path";
|
|
1131
1170
|
var cachedPolicy = null;
|
|
1171
|
+
var policyRoot = null;
|
|
1172
|
+
function resolvePolicyPath(projectPath) {
|
|
1173
|
+
return policyRoot ?? projectPath ?? process.cwd();
|
|
1174
|
+
}
|
|
1175
|
+
function configMtime(pp) {
|
|
1176
|
+
try {
|
|
1177
|
+
return statSync2(join7(pp, ".q-ring.json")).mtimeMs;
|
|
1178
|
+
} catch {
|
|
1179
|
+
return 0;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1132
1182
|
function loadPolicy(projectPath) {
|
|
1133
|
-
const pp = projectPath
|
|
1134
|
-
|
|
1183
|
+
const pp = resolvePolicyPath(projectPath);
|
|
1184
|
+
const mtimeMs = configMtime(pp);
|
|
1185
|
+
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1135
1186
|
return cachedPolicy.policy;
|
|
1136
1187
|
}
|
|
1137
1188
|
const config = readProjectConfig(pp);
|
|
1138
1189
|
const policy = config?.policy ?? {};
|
|
1139
|
-
cachedPolicy = { path: pp, policy };
|
|
1190
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1140
1191
|
return policy;
|
|
1141
1192
|
}
|
|
1142
1193
|
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
@@ -1205,7 +1256,13 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1205
1256
|
const policy = loadPolicy(projectPath);
|
|
1206
1257
|
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1207
1258
|
if (policy.exec.denyCommands) {
|
|
1208
|
-
const denied = policy.exec.denyCommands.find((d) =>
|
|
1259
|
+
const denied = policy.exec.denyCommands.find((d) => {
|
|
1260
|
+
const pattern = new RegExp(
|
|
1261
|
+
`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`,
|
|
1262
|
+
"i"
|
|
1263
|
+
);
|
|
1264
|
+
return pattern.test(command);
|
|
1265
|
+
});
|
|
1209
1266
|
if (denied) {
|
|
1210
1267
|
return {
|
|
1211
1268
|
allowed: false,
|
|
@@ -1215,7 +1272,8 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1215
1272
|
}
|
|
1216
1273
|
}
|
|
1217
1274
|
if (policy.exec.allowCommands) {
|
|
1218
|
-
const
|
|
1275
|
+
const normalized = command.trimStart();
|
|
1276
|
+
const allowed = policy.exec.allowCommands.some((a) => normalized.startsWith(a));
|
|
1219
1277
|
if (!allowed) {
|
|
1220
1278
|
return {
|
|
1221
1279
|
allowed: false,
|
|
@@ -1242,7 +1300,7 @@ function getPolicySummary(projectPath) {
|
|
|
1242
1300
|
// src/core/keyring.ts
|
|
1243
1301
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1244
1302
|
import { homedir as homedir5 } from "os";
|
|
1245
|
-
import { join as
|
|
1303
|
+
import { join as join8 } from "path";
|
|
1246
1304
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1247
1305
|
|
|
1248
1306
|
// src/utils/hash.ts
|
|
@@ -1368,7 +1426,7 @@ var awsStsProvider = {
|
|
|
1368
1426
|
expiresAt: creds.Expiration
|
|
1369
1427
|
};
|
|
1370
1428
|
} catch (err) {
|
|
1371
|
-
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}
|
|
1429
|
+
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1372
1430
|
}
|
|
1373
1431
|
}
|
|
1374
1432
|
};
|
|
@@ -1436,7 +1494,7 @@ req.end();
|
|
|
1436
1494
|
expiresAt: new Date(Date.now() + expiresInSeconds * 1e3).toISOString()
|
|
1437
1495
|
};
|
|
1438
1496
|
} catch (err) {
|
|
1439
|
-
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}
|
|
1497
|
+
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1440
1498
|
}
|
|
1441
1499
|
}
|
|
1442
1500
|
};
|
|
@@ -1446,10 +1504,10 @@ registry.register(httpProvider);
|
|
|
1446
1504
|
|
|
1447
1505
|
// src/core/keyring.ts
|
|
1448
1506
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1449
|
-
const dir =
|
|
1507
|
+
const dir = join8(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1450
1508
|
mkdirSync5(dir, { recursive: true });
|
|
1451
1509
|
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1452
|
-
const lockPath =
|
|
1510
|
+
const lockPath = join8(dir, `${safe}.lock`);
|
|
1453
1511
|
const deadline = Date.now() + 8e3;
|
|
1454
1512
|
while (Date.now() < deadline) {
|
|
1455
1513
|
try {
|
|
@@ -1833,6 +1891,16 @@ function exportSecrets(opts = {}) {
|
|
|
1833
1891
|
if (entry.envelope) {
|
|
1834
1892
|
const decay = checkDecay(entry.envelope);
|
|
1835
1893
|
if (decay.isExpired) continue;
|
|
1894
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
1895
|
+
logAudit({
|
|
1896
|
+
action: "read",
|
|
1897
|
+
key: entry.key,
|
|
1898
|
+
scope: entry.scope,
|
|
1899
|
+
source,
|
|
1900
|
+
detail: "blocked: requires user approval (export)"
|
|
1901
|
+
});
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1836
1904
|
const value = collapseValue(entry.envelope, env);
|
|
1837
1905
|
if (value !== null) {
|
|
1838
1906
|
rawValues.set(entry.key, value);
|
|
@@ -1964,7 +2032,7 @@ function tunnelList() {
|
|
|
1964
2032
|
|
|
1965
2033
|
// src/core/memory.ts
|
|
1966
2034
|
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1967
|
-
import { join as
|
|
2035
|
+
import { join as join9 } from "path";
|
|
1968
2036
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1969
2037
|
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1970
2038
|
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
@@ -1972,14 +2040,14 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
1972
2040
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
1973
2041
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
1974
2042
|
function getMemoryDir() {
|
|
1975
|
-
const dir =
|
|
2043
|
+
const dir = join9(homedir6(), ".config", "q-ring");
|
|
1976
2044
|
if (!existsSync6(dir)) {
|
|
1977
2045
|
mkdirSync6(dir, { recursive: true });
|
|
1978
2046
|
}
|
|
1979
2047
|
return dir;
|
|
1980
2048
|
}
|
|
1981
2049
|
function getMemoryPath() {
|
|
1982
|
-
return
|
|
2050
|
+
return join9(getMemoryDir(), MEMORY_FILE);
|
|
1983
2051
|
}
|
|
1984
2052
|
function deriveLegacyKey() {
|
|
1985
2053
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
@@ -2090,8 +2158,8 @@ export {
|
|
|
2090
2158
|
exportAudit,
|
|
2091
2159
|
detectAnomalies,
|
|
2092
2160
|
listEntanglements,
|
|
2093
|
-
httpRequest,
|
|
2094
2161
|
checkSSRF,
|
|
2162
|
+
httpRequest,
|
|
2095
2163
|
registerHook,
|
|
2096
2164
|
removeHook,
|
|
2097
2165
|
listHooks,
|
|
@@ -2124,4 +2192,4 @@ export {
|
|
|
2124
2192
|
forget,
|
|
2125
2193
|
clearMemory
|
|
2126
2194
|
};
|
|
2127
|
-
//# sourceMappingURL=chunk-
|
|
2195
|
+
//# sourceMappingURL=chunk-SRESNRML.js.map
|