@node9/proxy 1.0.0 → 1.0.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.js CHANGED
@@ -35,7 +35,7 @@ __export(src_exports, {
35
35
  module.exports = __toCommonJS(src_exports);
36
36
 
37
37
  // src/core.ts
38
- var import_chalk = __toESM(require("chalk"));
38
+ var import_chalk2 = __toESM(require("chalk"));
39
39
  var import_prompts = require("@inquirer/prompts");
40
40
  var import_fs = __toESM(require("fs"));
41
41
  var import_path = __toESM(require("path"));
@@ -45,19 +45,69 @@ var import_sh_syntax = require("sh-syntax");
45
45
 
46
46
  // src/ui/native.ts
47
47
  var import_child_process = require("child_process");
48
+ var import_chalk = __toESM(require("chalk"));
48
49
  var isTestEnv = () => {
49
50
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
50
51
  };
52
+ function smartTruncate(str, maxLen = 500) {
53
+ if (str.length <= maxLen) return str;
54
+ const edge = Math.floor(maxLen / 2) - 3;
55
+ return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
56
+ }
57
+ function formatArgs(args) {
58
+ if (args === null || args === void 0) return "(none)";
59
+ let parsed = args;
60
+ if (typeof args === "string") {
61
+ const trimmed = args.trim();
62
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
63
+ try {
64
+ parsed = JSON.parse(trimmed);
65
+ } catch {
66
+ parsed = args;
67
+ }
68
+ } else {
69
+ return smartTruncate(args, 600);
70
+ }
71
+ }
72
+ if (typeof parsed === "object" && !Array.isArray(parsed)) {
73
+ const obj = parsed;
74
+ const codeKeys = [
75
+ "command",
76
+ "cmd",
77
+ "shell_command",
78
+ "bash_command",
79
+ "script",
80
+ "code",
81
+ "input",
82
+ "sql",
83
+ "query",
84
+ "arguments",
85
+ "args",
86
+ "param",
87
+ "params",
88
+ "text"
89
+ ];
90
+ const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
91
+ if (foundKey) {
92
+ const val = obj[foundKey];
93
+ const str = typeof val === "string" ? val : JSON.stringify(val);
94
+ return `[${foundKey.toUpperCase()}]:
95
+ ${smartTruncate(str, 500)}`;
96
+ }
97
+ return Object.entries(obj).slice(0, 5).map(
98
+ ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
99
+ ).join("\n");
100
+ }
101
+ return smartTruncate(JSON.stringify(parsed), 200);
102
+ }
51
103
  function sendDesktopNotification(title, body) {
52
104
  if (isTestEnv()) return;
53
105
  try {
54
- const safeTitle = title.replace(/"/g, '\\"');
55
- const safeBody = body.replace(/"/g, '\\"');
56
106
  if (process.platform === "darwin") {
57
- const script = `display notification "${safeBody}" with title "${safeTitle}"`;
107
+ const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
58
108
  (0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
59
109
  } else if (process.platform === "linux") {
60
- (0, import_child_process.spawn)("notify-send", [safeTitle, safeBody, "--icon=dialog-warning"], {
110
+ (0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
61
111
  detached: true,
62
112
  stdio: "ignore"
63
113
  }).unref();
@@ -65,69 +115,28 @@ function sendDesktopNotification(title, body) {
65
115
  } catch {
66
116
  }
67
117
  }
68
- function formatArgs(args) {
69
- if (args === null || args === void 0) return "(none)";
70
- if (typeof args !== "object" || Array.isArray(args)) {
71
- const str = typeof args === "string" ? args : JSON.stringify(args);
72
- return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
73
- }
74
- const entries = Object.entries(args).filter(
75
- ([, v]) => v !== null && v !== void 0 && v !== ""
76
- );
77
- if (entries.length === 0) return "(none)";
78
- const MAX_FIELDS = 5;
79
- const MAX_VALUE_LEN = 120;
80
- const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
81
- const str = typeof val === "string" ? val : JSON.stringify(val);
82
- const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
83
- return ` ${key}: ${truncated}`;
84
- });
85
- if (entries.length > MAX_FIELDS) {
86
- lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
87
- }
88
- return lines.join("\n");
89
- }
90
118
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
91
119
  if (isTestEnv()) return "deny";
92
- if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
93
- console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
94
- console.log(`[DEBUG Native] isTestEnv check:`, {
95
- VITEST: process.env.VITEST,
96
- NODE_ENV: process.env.NODE_ENV,
97
- CI: process.env.CI,
98
- isTest: isTestEnv()
99
- });
100
- }
101
- const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
120
+ const formattedArgs = formatArgs(args);
121
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
102
122
  let message = "";
103
- if (locked) {
104
- message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
123
+ if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
105
124
  `;
106
- message += `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
125
+ message += `Tool: ${toolName}
107
126
  `;
108
- }
109
- message += `Tool: ${toolName}
110
- `;
111
- message += `Agent: ${agent || "AI Agent"}
112
- `;
113
- if (explainableLabel) {
114
- message += `Reason: ${explainableLabel}
127
+ message += `Agent: ${agent || "AI Agent"}
115
128
  `;
116
- }
117
- message += `
118
- Arguments:
119
- ${formatArgs(args)}`;
120
- if (!locked) {
121
- message += `
129
+ message += `Rule: ${explainableLabel || "Security Policy"}
122
130
 
123
- Enter = Allow | Click "Block" to deny`;
124
- }
125
- const safeMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "'");
126
- const safeTitle = title.replace(/"/g, '\\"');
131
+ `;
132
+ message += `${formattedArgs}`;
133
+ process.stderr.write(import_chalk.default.yellow(`
134
+ \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
135
+ `));
127
136
  return new Promise((resolve) => {
128
137
  let childProcess = null;
129
138
  const onAbort = () => {
130
- if (childProcess) {
139
+ if (childProcess && childProcess.pid) {
131
140
  try {
132
141
  process.kill(childProcess.pid, "SIGKILL");
133
142
  } catch {
@@ -139,83 +148,51 @@ Enter = Allow | Click "Block" to deny`;
139
148
  if (signal.aborted) return resolve("deny");
140
149
  signal.addEventListener("abort", onAbort);
141
150
  }
142
- const cleanup = () => {
143
- if (signal) signal.removeEventListener("abort", onAbort);
144
- };
145
151
  try {
146
152
  if (process.platform === "darwin") {
147
153
  const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
148
- const script = `
149
- tell application "System Events"
150
- activate
151
- display dialog "${safeMessage}" with title "${safeTitle}" ${buttons}
152
- end tell`;
153
- childProcess = (0, import_child_process.spawn)("osascript", ["-e", script]);
154
- let output = "";
155
- childProcess.stdout?.on("data", (d) => output += d.toString());
156
- childProcess.on("close", (code) => {
157
- cleanup();
158
- if (locked) return resolve("deny");
159
- if (code === 0) {
160
- if (output.includes("Always Allow")) return resolve("always_allow");
161
- if (output.includes("Allow")) return resolve("allow");
162
- }
163
- resolve("deny");
164
- });
154
+ const script = `on run argv
155
+ tell application "System Events"
156
+ activate
157
+ display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
158
+ end tell
159
+ end run`;
160
+ childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
165
161
  } else if (process.platform === "linux") {
166
- const argsList = locked ? [
167
- "--info",
168
- "--title",
169
- title,
170
- "--text",
171
- safeMessage,
172
- "--ok-label",
173
- "Waiting for Slack\u2026",
174
- "--timeout",
175
- "300"
176
- ] : [
177
- "--question",
162
+ const argsList = [
163
+ locked ? "--info" : "--question",
164
+ "--modal",
165
+ "--width=450",
178
166
  "--title",
179
167
  title,
180
168
  "--text",
181
- safeMessage,
169
+ message,
182
170
  "--ok-label",
183
- "Allow",
184
- "--cancel-label",
185
- "Block",
186
- "--extra-button",
187
- "Always Allow",
171
+ locked ? "Waiting..." : "Allow",
188
172
  "--timeout",
189
173
  "300"
190
174
  ];
175
+ if (!locked) {
176
+ argsList.push("--cancel-label", "Block");
177
+ argsList.push("--extra-button", "Always Allow");
178
+ }
191
179
  childProcess = (0, import_child_process.spawn)("zenity", argsList);
192
- let output = "";
193
- childProcess.stdout?.on("data", (d) => output += d.toString());
194
- childProcess.on("close", (code) => {
195
- cleanup();
196
- if (locked) return resolve("deny");
197
- if (output.trim() === "Always Allow") return resolve("always_allow");
198
- if (code === 0) return resolve("allow");
199
- resolve("deny");
200
- });
201
180
  } else if (process.platform === "win32") {
202
- const buttonType = locked ? "OK" : "YesNo";
203
- const ps = `
204
- Add-Type -AssemblyName PresentationFramework;
205
- $res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
206
- if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
181
+ const b64Msg = Buffer.from(message).toString("base64");
182
+ const b64Title = Buffer.from(title).toString("base64");
183
+ const ps = `Add-Type -AssemblyName PresentationFramework; $msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Msg}")); $title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Title}")); $res = [System.Windows.MessageBox]::Show($msg, $title, "${locked ? "OK" : "YesNo"}", "Warning", "Button2", "DefaultDesktopOnly"); if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
207
184
  childProcess = (0, import_child_process.spawn)("powershell", ["-Command", ps]);
208
- childProcess.on("close", (code) => {
209
- cleanup();
210
- if (locked) return resolve("deny");
211
- resolve(code === 0 ? "allow" : "deny");
212
- });
213
- } else {
214
- cleanup();
215
- resolve("deny");
216
185
  }
186
+ let output = "";
187
+ childProcess?.stdout?.on("data", (d) => output += d.toString());
188
+ childProcess?.on("close", (code) => {
189
+ if (signal) signal.removeEventListener("abort", onAbort);
190
+ if (locked) return resolve("deny");
191
+ if (output.includes("Always Allow")) return resolve("always_allow");
192
+ if (code === 0) return resolve("allow");
193
+ resolve("deny");
194
+ });
217
195
  } catch {
218
- cleanup();
219
196
  resolve("deny");
220
197
  }
221
198
  });
@@ -693,8 +670,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
693
670
  const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
694
671
  const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
695
672
  console.error(
696
- import_chalk.default.yellow(`
697
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk.default.dim(`
673
+ import_chalk2.default.yellow(`
674
+ \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
698
675
  Falling back to local rules...
699
676
  `)
700
677
  );
@@ -702,13 +679,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
702
679
  }
703
680
  if (cloudEnforced && cloudRequestId) {
704
681
  console.error(
705
- import_chalk.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
682
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
706
683
  );
707
- console.error(import_chalk.default.cyan(" Dashboard \u2192 ") + import_chalk.default.bold("Mission Control > Activity Feed\n"));
684
+ console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
708
685
  } else if (!cloudEnforced) {
709
686
  const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
710
687
  console.error(
711
- import_chalk.default.dim(`
688
+ import_chalk2.default.dim(`
712
689
  \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
713
690
  `)
714
691
  );
@@ -773,9 +750,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
773
750
  try {
774
751
  if (!approvers.native && !cloudEnforced) {
775
752
  console.error(
776
- import_chalk.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
753
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
777
754
  );
778
- console.error(import_chalk.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
755
+ console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
779
756
  `));
780
757
  }
781
758
  const daemonDecision = await askDaemon(toolName, args, meta, signal);
@@ -798,11 +775,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
798
775
  racePromises.push(
799
776
  (async () => {
800
777
  try {
801
- console.log(import_chalk.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
802
- console.log(`${import_chalk.default.bold("Action:")} ${import_chalk.default.red(toolName)}`);
803
- console.log(`${import_chalk.default.bold("Flagged By:")} ${import_chalk.default.yellow(explainableLabel)}`);
778
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
779
+ console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
780
+ console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
804
781
  if (isRemoteLocked) {
805
- console.log(import_chalk.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
782
+ console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
806
783
  `));
807
784
  await new Promise((_, reject) => {
808
785
  signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
@@ -1053,11 +1030,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
1053
1030
  if (!statusRes.ok) continue;
1054
1031
  const { status, reason } = await statusRes.json();
1055
1032
  if (status === "APPROVED") {
1056
- console.error(import_chalk.default.green("\u2705 Approved via Cloud.\n"));
1033
+ console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
1057
1034
  return { approved: true, reason };
1058
1035
  }
1059
1036
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
1060
- console.error(import_chalk.default.red("\u274C Denied via Cloud.\n"));
1037
+ console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
1061
1038
  return { approved: false, reason };
1062
1039
  }
1063
1040
  } catch {