@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/cli.js +200 -223
- package/dist/cli.mjs +200 -223
- package/dist/index.js +109 -132
- package/dist/index.mjs +109 -132
- package/package.json +30 -1
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
|
|
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 "${
|
|
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", [
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 =
|
|
167
|
-
"--info",
|
|
168
|
-
"--
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
203
|
-
const
|
|
204
|
-
|
|
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
|
-
|
|
697
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
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
|
-
|
|
682
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
706
683
|
);
|
|
707
|
-
console.error(
|
|
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
|
-
|
|
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
|
-
|
|
753
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
777
754
|
);
|
|
778
|
-
console.error(
|
|
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(
|
|
802
|
-
console.log(`${
|
|
803
|
-
console.log(`${
|
|
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(
|
|
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(
|
|
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(
|
|
1037
|
+
console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
|
|
1061
1038
|
return { approved: false, reason };
|
|
1062
1039
|
}
|
|
1063
1040
|
} catch {
|