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