@node9/proxy 1.0.19 → 1.1.1

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/dist/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  // src/core.ts
2
2
  import chalk2 from "chalk";
3
3
  import { confirm } from "@inquirer/prompts";
4
- import fs2 from "fs";
5
- import path4 from "path";
4
+ import fs3 from "fs";
5
+ import path5 from "path";
6
6
  import os2 from "os";
7
7
  import net from "net";
8
8
  import { randomUUID } from "crypto";
9
9
  import { spawnSync } from "child_process";
10
10
  import pm from "picomatch";
11
+ import safeRegex from "safe-regex2";
11
12
  import { parse } from "sh-syntax";
12
13
 
13
14
  // src/ui/native.ts
@@ -429,8 +430,8 @@ function sanitizeConfig(raw) {
429
430
  }
430
431
  }
431
432
  const lines = result.error.issues.map((issue) => {
432
- const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
433
- return ` \u2022 ${path5}: ${issue.message}`;
433
+ const path6 = issue.path.length > 0 ? issue.path.join(".") : "root";
434
+ return ` \u2022 ${path6}: ${issue.message}`;
434
435
  });
435
436
  return {
436
437
  sanitized,
@@ -643,10 +644,14 @@ function readActiveShields() {
643
644
  }
644
645
 
645
646
  // src/dlp.ts
647
+ import fs2 from "fs";
648
+ import path4 from "path";
646
649
  var DLP_PATTERNS = [
647
650
  { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
648
651
  { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
649
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
652
+ // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
653
+ // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
654
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
650
655
  { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
651
656
  { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
652
657
  {
@@ -654,8 +659,76 @@ var DLP_PATTERNS = [
654
659
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
655
660
  severity: "block"
656
661
  },
662
+ // GCP service account JSON (detects the type field that uniquely identifies it)
663
+ {
664
+ name: "GCP Service Account",
665
+ regex: /"type"\s*:\s*"service_account"/,
666
+ severity: "block"
667
+ },
668
+ // NPM auth token in .npmrc format
669
+ {
670
+ name: "NPM Auth Token",
671
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
672
+ severity: "block"
673
+ },
657
674
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
658
675
  ];
676
+ var SENSITIVE_PATH_PATTERNS = [
677
+ /[/\\]\.ssh[/\\]/i,
678
+ /[/\\]\.aws[/\\]/i,
679
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
680
+ /[/\\]\.azure[/\\]/i,
681
+ /[/\\]\.kube[/\\]config$/i,
682
+ /[/\\]\.env($|\.)/i,
683
+ // .env, .env.local, .env.production — not .envoy
684
+ /[/\\]\.git-credentials$/i,
685
+ /[/\\]\.npmrc$/i,
686
+ /[/\\]\.docker[/\\]config\.json$/i,
687
+ /[/\\][^/\\]+\.pem$/i,
688
+ /[/\\][^/\\]+\.key$/i,
689
+ /[/\\][^/\\]+\.p12$/i,
690
+ /[/\\][^/\\]+\.pfx$/i,
691
+ /^\/etc\/passwd$/,
692
+ /^\/etc\/shadow$/,
693
+ /^\/etc\/sudoers$/,
694
+ /[/\\]credentials\.json$/i,
695
+ /[/\\]id_rsa$/i,
696
+ /[/\\]id_ed25519$/i,
697
+ /[/\\]id_ecdsa$/i
698
+ ];
699
+ function scanFilePath(filePath, cwd = process.cwd()) {
700
+ if (!filePath) return null;
701
+ let resolved;
702
+ try {
703
+ const absolute = path4.resolve(cwd, filePath);
704
+ resolved = fs2.realpathSync.native(absolute);
705
+ } catch (err) {
706
+ const code = err.code;
707
+ if (code === "ENOENT" || code === "ENOTDIR") {
708
+ resolved = path4.resolve(cwd, filePath);
709
+ } else {
710
+ return {
711
+ patternName: "Sensitive File Path",
712
+ fieldPath: "file_path",
713
+ redactedSample: filePath,
714
+ severity: "block"
715
+ };
716
+ }
717
+ }
718
+ const normalised = resolved.replace(/\\/g, "/");
719
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
720
+ if (pattern.test(normalised)) {
721
+ return {
722
+ patternName: "Sensitive File Path",
723
+ fieldPath: "file_path",
724
+ redactedSample: filePath,
725
+ // show original path in alert, not resolved
726
+ severity: "block"
727
+ };
728
+ }
729
+ }
730
+ return null;
731
+ }
659
732
  function maskSecret(raw, pattern) {
660
733
  const match = raw.match(pattern);
661
734
  if (!match) return "****";
@@ -713,17 +786,17 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
713
786
  }
714
787
 
715
788
  // src/core.ts
716
- var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
717
- var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
718
- var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
719
- var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
789
+ var PAUSED_FILE = path5.join(os2.homedir(), ".node9", "PAUSED");
790
+ var TRUST_FILE = path5.join(os2.homedir(), ".node9", "trust.json");
791
+ var LOCAL_AUDIT_LOG = path5.join(os2.homedir(), ".node9", "audit.log");
792
+ var HOOK_DEBUG_LOG = path5.join(os2.homedir(), ".node9", "hook-debug.log");
720
793
  function checkPause() {
721
794
  try {
722
- if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
723
- const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
795
+ if (!fs3.existsSync(PAUSED_FILE)) return { paused: false };
796
+ const state = JSON.parse(fs3.readFileSync(PAUSED_FILE, "utf-8"));
724
797
  if (state.expiry > 0 && Date.now() >= state.expiry) {
725
798
  try {
726
- fs2.unlinkSync(PAUSED_FILE);
799
+ fs3.unlinkSync(PAUSED_FILE);
727
800
  } catch {
728
801
  }
729
802
  return { paused: false };
@@ -734,20 +807,66 @@ function checkPause() {
734
807
  }
735
808
  }
736
809
  function atomicWriteSync(filePath, data, options) {
737
- const dir = path4.dirname(filePath);
738
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
810
+ const dir = path5.dirname(filePath);
811
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
739
812
  const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
740
- fs2.writeFileSync(tmpPath, data, options);
741
- fs2.renameSync(tmpPath, filePath);
813
+ fs3.writeFileSync(tmpPath, data, options);
814
+ fs3.renameSync(tmpPath, filePath);
815
+ }
816
+ var MAX_REGEX_LENGTH = 100;
817
+ var REGEX_CACHE_MAX = 500;
818
+ var regexCache = /* @__PURE__ */ new Map();
819
+ function validateRegex(pattern) {
820
+ if (!pattern) return "Pattern is required";
821
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
822
+ try {
823
+ new RegExp(pattern);
824
+ } catch (e) {
825
+ return `Invalid regex syntax: ${e.message}`;
826
+ }
827
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
828
+ if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
829
+ return null;
830
+ }
831
+ function getCompiledRegex(pattern, flags = "") {
832
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
833
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
834
+ return null;
835
+ }
836
+ const key = `${pattern}\0${flags}`;
837
+ if (regexCache.has(key)) {
838
+ const cached = regexCache.get(key);
839
+ regexCache.delete(key);
840
+ regexCache.set(key, cached);
841
+ return cached;
842
+ }
843
+ const err = validateRegex(pattern);
844
+ if (err) {
845
+ if (process.env.NODE9_DEBUG === "1")
846
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
847
+ return null;
848
+ }
849
+ try {
850
+ const re = new RegExp(pattern, flags);
851
+ if (regexCache.size >= REGEX_CACHE_MAX) {
852
+ const oldest = regexCache.keys().next().value;
853
+ if (oldest) regexCache.delete(oldest);
854
+ }
855
+ regexCache.set(key, re);
856
+ return re;
857
+ } catch (e) {
858
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
859
+ return null;
860
+ }
742
861
  }
743
862
  function getActiveTrustSession(toolName) {
744
863
  try {
745
- if (!fs2.existsSync(TRUST_FILE)) return false;
746
- const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
864
+ if (!fs3.existsSync(TRUST_FILE)) return false;
865
+ const trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
747
866
  const now = Date.now();
748
867
  const active = trust.entries.filter((e) => e.expiry > now);
749
868
  if (active.length !== trust.entries.length) {
750
- fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
869
+ fs3.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
751
870
  }
752
871
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
753
872
  } catch {
@@ -758,8 +877,8 @@ function writeTrustSession(toolName, durationMs) {
758
877
  try {
759
878
  let trust = { entries: [] };
760
879
  try {
761
- if (fs2.existsSync(TRUST_FILE)) {
762
- trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
880
+ if (fs3.existsSync(TRUST_FILE)) {
881
+ trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
763
882
  }
764
883
  } catch {
765
884
  }
@@ -775,9 +894,9 @@ function writeTrustSession(toolName, durationMs) {
775
894
  }
776
895
  function appendToLog(logPath, entry) {
777
896
  try {
778
- const dir = path4.dirname(logPath);
779
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
780
- fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
897
+ const dir = path5.dirname(logPath);
898
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
899
+ fs3.appendFileSync(logPath, JSON.stringify(entry) + "\n");
781
900
  } catch {
782
901
  }
783
902
  }
@@ -819,9 +938,9 @@ function matchesPattern(text, patterns) {
819
938
  const withoutDotSlash = text.replace(/^\.\//, "");
820
939
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
821
940
  }
822
- function getNestedValue(obj, path5) {
941
+ function getNestedValue(obj, path6) {
823
942
  if (!obj || typeof obj !== "object") return null;
824
- return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
943
+ return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
825
944
  }
826
945
  function evaluateSmartConditions(args, rule) {
827
946
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -840,19 +959,16 @@ function evaluateSmartConditions(args, rule) {
840
959
  return val !== null && cond.value ? !val.includes(cond.value) : true;
841
960
  case "matches": {
842
961
  if (val === null || !cond.value) return false;
843
- try {
844
- return new RegExp(cond.value, cond.flags ?? "").test(val);
845
- } catch {
846
- return false;
847
- }
962
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
963
+ if (!reM) return false;
964
+ return reM.test(val);
848
965
  }
849
966
  case "notMatches": {
850
- if (val === null || !cond.value) return true;
851
- try {
852
- return !new RegExp(cond.value, cond.flags ?? "").test(val);
853
- } catch {
854
- return true;
855
- }
967
+ if (!cond.value) return false;
968
+ if (val === null) return true;
969
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
970
+ if (!reN) return false;
971
+ return !reN.test(val);
856
972
  }
857
973
  case "matchesGlob":
858
974
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -1174,9 +1290,9 @@ var ADVISORY_SMART_RULES = [
1174
1290
  var cachedConfig = null;
1175
1291
  function getInternalToken() {
1176
1292
  try {
1177
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1178
- if (!fs2.existsSync(pidFile)) return null;
1179
- const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1293
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1294
+ if (!fs3.existsSync(pidFile)) return null;
1295
+ const data = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1180
1296
  process.kill(data.pid, 0);
1181
1297
  return data.internalToken ?? null;
1182
1298
  } catch {
@@ -1296,10 +1412,10 @@ function isIgnoredTool(toolName) {
1296
1412
  var DAEMON_PORT = 7391;
1297
1413
  var DAEMON_HOST = "127.0.0.1";
1298
1414
  function isDaemonRunning() {
1299
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1300
- if (fs2.existsSync(pidFile)) {
1415
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1416
+ if (fs3.existsSync(pidFile)) {
1301
1417
  try {
1302
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1418
+ const { pid, port } = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1303
1419
  if (port !== DAEMON_PORT) return false;
1304
1420
  process.kill(pid, 0);
1305
1421
  return true;
@@ -1319,9 +1435,9 @@ function isDaemonRunning() {
1319
1435
  }
1320
1436
  function getPersistentDecision(toolName) {
1321
1437
  try {
1322
- const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1323
- if (!fs2.existsSync(file)) return null;
1324
- const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1438
+ const file = path5.join(os2.homedir(), ".node9", "decisions.json");
1439
+ if (!fs3.existsSync(file)) return null;
1440
+ const decisions = JSON.parse(fs3.readFileSync(file, "utf-8"));
1325
1441
  const d = decisions[toolName];
1326
1442
  if (d === "allow" || d === "deny") return d;
1327
1443
  } catch {
@@ -1403,7 +1519,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1403
1519
  signal: AbortSignal.timeout(3e3)
1404
1520
  });
1405
1521
  }
1406
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
1522
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path5.join(os2.tmpdir(), "node9-activity.sock");
1407
1523
  function notifyActivity(data) {
1408
1524
  return new Promise((resolve) => {
1409
1525
  try {
@@ -1465,7 +1581,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1465
1581
  let policyMatchedWord;
1466
1582
  let riskMetadata;
1467
1583
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1468
- const dlpMatch = scanArgs(args);
1584
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1585
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1586
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1469
1587
  if (dlpMatch) {
1470
1588
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1471
1589
  if (dlpMatch.severity === "block") {
@@ -1837,10 +1955,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1837
1955
  }
1838
1956
  return finalResult;
1839
1957
  }
1840
- function getConfig() {
1841
- if (cachedConfig) return cachedConfig;
1842
- const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1843
- const projectPath = path4.join(process.cwd(), "node9.config.json");
1958
+ function getConfig(cwd) {
1959
+ if (!cwd && cachedConfig) return cachedConfig;
1960
+ const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
1961
+ const projectPath = path5.join(cwd ?? process.cwd(), "node9.config.json");
1844
1962
  const globalConfig = tryLoadConfig(globalPath);
1845
1963
  const projectConfig = tryLoadConfig(projectPath);
1846
1964
  const mergedSettings = {
@@ -1927,18 +2045,19 @@ function getConfig() {
1927
2045
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1928
2046
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1929
2047
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1930
- cachedConfig = {
2048
+ const result = {
1931
2049
  settings: mergedSettings,
1932
2050
  policy: mergedPolicy,
1933
2051
  environments: mergedEnvironments
1934
2052
  };
1935
- return cachedConfig;
2053
+ if (!cwd) cachedConfig = result;
2054
+ return result;
1936
2055
  }
1937
2056
  function tryLoadConfig(filePath) {
1938
- if (!fs2.existsSync(filePath)) return null;
2057
+ if (!fs3.existsSync(filePath)) return null;
1939
2058
  let raw;
1940
2059
  try {
1941
- raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2060
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1942
2061
  } catch (err) {
1943
2062
  const msg = err instanceof Error ? err.message : String(err);
1944
2063
  process.stderr.write(
@@ -2000,9 +2119,9 @@ function getCredentials() {
2000
2119
  };
2001
2120
  }
2002
2121
  try {
2003
- const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
2004
- if (fs2.existsSync(credPath)) {
2005
- const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
2122
+ const credPath = path5.join(os2.homedir(), ".node9", "credentials.json");
2123
+ if (fs3.existsSync(credPath)) {
2124
+ const creds = JSON.parse(fs3.readFileSync(credPath, "utf-8"));
2006
2125
  const profileName = process.env.NODE9_PROFILE || "default";
2007
2126
  const profile = creds[profileName];
2008
2127
  if (profile?.apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.19",
3
+ "version": "1.1.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -71,6 +71,7 @@
71
71
  "commander": "^14.0.3",
72
72
  "execa": "^9.6.1",
73
73
  "picomatch": "^4.0.3",
74
+ "safe-regex2": "^5.1.0",
74
75
  "sh-syntax": "^0.5.8",
75
76
  "zod": "^3.25.76"
76
77
  },