@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core.ts
2
- import chalk from "chalk";
2
+ import chalk2 from "chalk";
3
3
  import { confirm } from "@inquirer/prompts";
4
4
  import fs from "fs";
5
5
  import path from "path";
@@ -9,19 +9,69 @@ import { parse } from "sh-syntax";
9
9
 
10
10
  // src/ui/native.ts
11
11
  import { spawn } from "child_process";
12
+ import chalk from "chalk";
12
13
  var isTestEnv = () => {
13
14
  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";
14
15
  };
16
+ function smartTruncate(str, maxLen = 500) {
17
+ if (str.length <= maxLen) return str;
18
+ const edge = Math.floor(maxLen / 2) - 3;
19
+ return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
20
+ }
21
+ function formatArgs(args) {
22
+ if (args === null || args === void 0) return "(none)";
23
+ let parsed = args;
24
+ if (typeof args === "string") {
25
+ const trimmed = args.trim();
26
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
27
+ try {
28
+ parsed = JSON.parse(trimmed);
29
+ } catch {
30
+ parsed = args;
31
+ }
32
+ } else {
33
+ return smartTruncate(args, 600);
34
+ }
35
+ }
36
+ if (typeof parsed === "object" && !Array.isArray(parsed)) {
37
+ const obj = parsed;
38
+ const codeKeys = [
39
+ "command",
40
+ "cmd",
41
+ "shell_command",
42
+ "bash_command",
43
+ "script",
44
+ "code",
45
+ "input",
46
+ "sql",
47
+ "query",
48
+ "arguments",
49
+ "args",
50
+ "param",
51
+ "params",
52
+ "text"
53
+ ];
54
+ const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
55
+ if (foundKey) {
56
+ const val = obj[foundKey];
57
+ const str = typeof val === "string" ? val : JSON.stringify(val);
58
+ return `[${foundKey.toUpperCase()}]:
59
+ ${smartTruncate(str, 500)}`;
60
+ }
61
+ return Object.entries(obj).slice(0, 5).map(
62
+ ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
63
+ ).join("\n");
64
+ }
65
+ return smartTruncate(JSON.stringify(parsed), 200);
66
+ }
15
67
  function sendDesktopNotification(title, body) {
16
68
  if (isTestEnv()) return;
17
69
  try {
18
- const safeTitle = title.replace(/"/g, '\\"');
19
- const safeBody = body.replace(/"/g, '\\"');
20
70
  if (process.platform === "darwin") {
21
- const script = `display notification "${safeBody}" with title "${safeTitle}"`;
71
+ const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
22
72
  spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
23
73
  } else if (process.platform === "linux") {
24
- spawn("notify-send", [safeTitle, safeBody, "--icon=dialog-warning"], {
74
+ spawn("notify-send", [title, body, "--icon=dialog-warning"], {
25
75
  detached: true,
26
76
  stdio: "ignore"
27
77
  }).unref();
@@ -29,69 +79,28 @@ function sendDesktopNotification(title, body) {
29
79
  } catch {
30
80
  }
31
81
  }
32
- function formatArgs(args) {
33
- if (args === null || args === void 0) return "(none)";
34
- if (typeof args !== "object" || Array.isArray(args)) {
35
- const str = typeof args === "string" ? args : JSON.stringify(args);
36
- return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
37
- }
38
- const entries = Object.entries(args).filter(
39
- ([, v]) => v !== null && v !== void 0 && v !== ""
40
- );
41
- if (entries.length === 0) return "(none)";
42
- const MAX_FIELDS = 5;
43
- const MAX_VALUE_LEN = 120;
44
- const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
45
- const str = typeof val === "string" ? val : JSON.stringify(val);
46
- const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
47
- return ` ${key}: ${truncated}`;
48
- });
49
- if (entries.length > MAX_FIELDS) {
50
- lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
51
- }
52
- return lines.join("\n");
53
- }
54
82
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
55
83
  if (isTestEnv()) return "deny";
56
- if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
57
- console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
58
- console.log(`[DEBUG Native] isTestEnv check:`, {
59
- VITEST: process.env.VITEST,
60
- NODE_ENV: process.env.NODE_ENV,
61
- CI: process.env.CI,
62
- isTest: isTestEnv()
63
- });
64
- }
65
- const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
84
+ const formattedArgs = formatArgs(args);
85
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
66
86
  let message = "";
67
- if (locked) {
68
- message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
87
+ if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
69
88
  `;
70
- 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
89
+ message += `Tool: ${toolName}
71
90
  `;
72
- }
73
- message += `Tool: ${toolName}
74
- `;
75
- message += `Agent: ${agent || "AI Agent"}
76
- `;
77
- if (explainableLabel) {
78
- message += `Reason: ${explainableLabel}
91
+ message += `Agent: ${agent || "AI Agent"}
79
92
  `;
80
- }
81
- message += `
82
- Arguments:
83
- ${formatArgs(args)}`;
84
- if (!locked) {
85
- message += `
93
+ message += `Rule: ${explainableLabel || "Security Policy"}
86
94
 
87
- Enter = Allow | Click "Block" to deny`;
88
- }
89
- const safeMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "'");
90
- const safeTitle = title.replace(/"/g, '\\"');
95
+ `;
96
+ message += `${formattedArgs}`;
97
+ process.stderr.write(chalk.yellow(`
98
+ \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
99
+ `));
91
100
  return new Promise((resolve) => {
92
101
  let childProcess = null;
93
102
  const onAbort = () => {
94
- if (childProcess) {
103
+ if (childProcess && childProcess.pid) {
95
104
  try {
96
105
  process.kill(childProcess.pid, "SIGKILL");
97
106
  } catch {
@@ -103,83 +112,51 @@ Enter = Allow | Click "Block" to deny`;
103
112
  if (signal.aborted) return resolve("deny");
104
113
  signal.addEventListener("abort", onAbort);
105
114
  }
106
- const cleanup = () => {
107
- if (signal) signal.removeEventListener("abort", onAbort);
108
- };
109
115
  try {
110
116
  if (process.platform === "darwin") {
111
117
  const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
112
- const script = `
113
- tell application "System Events"
114
- activate
115
- display dialog "${safeMessage}" with title "${safeTitle}" ${buttons}
116
- end tell`;
117
- childProcess = spawn("osascript", ["-e", script]);
118
- let output = "";
119
- childProcess.stdout?.on("data", (d) => output += d.toString());
120
- childProcess.on("close", (code) => {
121
- cleanup();
122
- if (locked) return resolve("deny");
123
- if (code === 0) {
124
- if (output.includes("Always Allow")) return resolve("always_allow");
125
- if (output.includes("Allow")) return resolve("allow");
126
- }
127
- resolve("deny");
128
- });
118
+ const script = `on run argv
119
+ tell application "System Events"
120
+ activate
121
+ display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
122
+ end tell
123
+ end run`;
124
+ childProcess = spawn("osascript", ["-e", script, "--", message, title]);
129
125
  } else if (process.platform === "linux") {
130
- const argsList = locked ? [
131
- "--info",
132
- "--title",
133
- title,
134
- "--text",
135
- safeMessage,
136
- "--ok-label",
137
- "Waiting for Slack\u2026",
138
- "--timeout",
139
- "300"
140
- ] : [
141
- "--question",
126
+ const argsList = [
127
+ locked ? "--info" : "--question",
128
+ "--modal",
129
+ "--width=450",
142
130
  "--title",
143
131
  title,
144
132
  "--text",
145
- safeMessage,
133
+ message,
146
134
  "--ok-label",
147
- "Allow",
148
- "--cancel-label",
149
- "Block",
150
- "--extra-button",
151
- "Always Allow",
135
+ locked ? "Waiting..." : "Allow",
152
136
  "--timeout",
153
137
  "300"
154
138
  ];
139
+ if (!locked) {
140
+ argsList.push("--cancel-label", "Block");
141
+ argsList.push("--extra-button", "Always Allow");
142
+ }
155
143
  childProcess = spawn("zenity", argsList);
156
- let output = "";
157
- childProcess.stdout?.on("data", (d) => output += d.toString());
158
- childProcess.on("close", (code) => {
159
- cleanup();
160
- if (locked) return resolve("deny");
161
- if (output.trim() === "Always Allow") return resolve("always_allow");
162
- if (code === 0) return resolve("allow");
163
- resolve("deny");
164
- });
165
144
  } else if (process.platform === "win32") {
166
- const buttonType = locked ? "OK" : "YesNo";
167
- const ps = `
168
- Add-Type -AssemblyName PresentationFramework;
169
- $res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
170
- if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
145
+ const b64Msg = Buffer.from(message).toString("base64");
146
+ const b64Title = Buffer.from(title).toString("base64");
147
+ 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 }`;
171
148
  childProcess = spawn("powershell", ["-Command", ps]);
172
- childProcess.on("close", (code) => {
173
- cleanup();
174
- if (locked) return resolve("deny");
175
- resolve(code === 0 ? "allow" : "deny");
176
- });
177
- } else {
178
- cleanup();
179
- resolve("deny");
180
149
  }
150
+ let output = "";
151
+ childProcess?.stdout?.on("data", (d) => output += d.toString());
152
+ childProcess?.on("close", (code) => {
153
+ if (signal) signal.removeEventListener("abort", onAbort);
154
+ if (locked) return resolve("deny");
155
+ if (output.includes("Always Allow")) return resolve("always_allow");
156
+ if (code === 0) return resolve("allow");
157
+ resolve("deny");
158
+ });
181
159
  } catch {
182
- cleanup();
183
160
  resolve("deny");
184
161
  }
185
162
  });
@@ -657,8 +634,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
657
634
  const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
658
635
  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;
659
636
  console.error(
660
- chalk.yellow(`
661
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk.dim(`
637
+ chalk2.yellow(`
638
+ \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
662
639
  Falling back to local rules...
663
640
  `)
664
641
  );
@@ -666,13 +643,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
666
643
  }
667
644
  if (cloudEnforced && cloudRequestId) {
668
645
  console.error(
669
- chalk.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
646
+ chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
670
647
  );
671
- console.error(chalk.cyan(" Dashboard \u2192 ") + chalk.bold("Mission Control > Activity Feed\n"));
648
+ console.error(chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n"));
672
649
  } else if (!cloudEnforced) {
673
650
  const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
674
651
  console.error(
675
- chalk.dim(`
652
+ chalk2.dim(`
676
653
  \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
677
654
  `)
678
655
  );
@@ -737,9 +714,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
737
714
  try {
738
715
  if (!approvers.native && !cloudEnforced) {
739
716
  console.error(
740
- chalk.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
717
+ chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
741
718
  );
742
- console.error(chalk.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
719
+ console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
743
720
  `));
744
721
  }
745
722
  const daemonDecision = await askDaemon(toolName, args, meta, signal);
@@ -762,11 +739,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
762
739
  racePromises.push(
763
740
  (async () => {
764
741
  try {
765
- console.log(chalk.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
766
- console.log(`${chalk.bold("Action:")} ${chalk.red(toolName)}`);
767
- console.log(`${chalk.bold("Flagged By:")} ${chalk.yellow(explainableLabel)}`);
742
+ console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
743
+ console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
744
+ console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
768
745
  if (isRemoteLocked) {
769
- console.log(chalk.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
746
+ console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
770
747
  `));
771
748
  await new Promise((_, reject) => {
772
749
  signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
@@ -1017,11 +994,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
1017
994
  if (!statusRes.ok) continue;
1018
995
  const { status, reason } = await statusRes.json();
1019
996
  if (status === "APPROVED") {
1020
- console.error(chalk.green("\u2705 Approved via Cloud.\n"));
997
+ console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
1021
998
  return { approved: true, reason };
1022
999
  }
1023
1000
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
1024
- console.error(chalk.red("\u274C Denied via Cloud.\n"));
1001
+ console.error(chalk2.red("\u274C Denied via Cloud.\n"));
1025
1002
  return { approved: false, reason };
1026
1003
  }
1027
1004
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -90,5 +90,34 @@
90
90
  },
91
91
  "publishConfig": {
92
92
  "access": "public"
93
+ },
94
+ "release": {
95
+ "branches": [
96
+ "main"
97
+ ],
98
+ "plugins": [
99
+ "@semantic-release/commit-analyzer",
100
+ "@semantic-release/release-notes-generator",
101
+ "@semantic-release/npm",
102
+ [
103
+ "@semantic-release/github",
104
+ {
105
+ "assets": [
106
+ "dist/*.js",
107
+ "dist/*.mjs"
108
+ ]
109
+ }
110
+ ],
111
+ [
112
+ "@semantic-release/git",
113
+ {
114
+ "assets": [
115
+ "package.json",
116
+ "package-lock.json"
117
+ ],
118
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
119
+ }
120
+ ]
121
+ ]
93
122
  }
94
123
  }