@i4ctime/q-ring 0.11.7 → 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.
@@ -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 ?? process.cwd();
1071
- if (cachedPolicy && cachedPolicy.path === pp) {
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) => command.includes(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 allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
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 join7 } from "path";
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 = join7(homedir5(), ".config", "q-ring", "jit-locks");
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 = join7(dir, `${safe}.lock`);
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 join8 } from "path";
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 = join8(homedir6(), ".config", "q-ring");
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 join8(getMemoryDir(), MEMORY_FILE);
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-MQM4DLPI.js.map
2152
+ //# sourceMappingURL=chunk-YWDKAZWS.js.map