@node9/proxy 1.1.5 → 1.1.7

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,6 +1,4 @@
1
1
  // src/core.ts
2
- import chalk2 from "chalk";
3
- import { confirm } from "@inquirer/prompts";
4
2
  import fs3 from "fs";
5
3
  import path5 from "path";
6
4
  import os2 from "os";
@@ -14,7 +12,6 @@ import { parse } from "sh-syntax";
14
12
  // src/ui/native.ts
15
13
  import { spawn } from "child_process";
16
14
  import path2 from "path";
17
- import chalk from "chalk";
18
15
 
19
16
  // src/context-sniper.ts
20
17
  import path from "path";
@@ -200,21 +197,6 @@ ${smartTruncate(str, 500)}`
200
197
  }
201
198
  return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
202
199
  }
203
- function sendDesktopNotification(title, body) {
204
- if (isTestEnv()) return;
205
- try {
206
- if (process.platform === "darwin") {
207
- const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
208
- spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
209
- } else if (process.platform === "linux") {
210
- spawn("notify-send", [title, body, "--icon=dialog-warning"], {
211
- detached: true,
212
- stdio: "ignore"
213
- }).unref();
214
- }
215
- } catch {
216
- }
217
- }
218
200
  function escapePango(text) {
219
201
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
220
202
  }
@@ -257,9 +239,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
257
239
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
258
240
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
259
241
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
260
- process.stderr.write(chalk.yellow(`
261
- \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
262
- `));
263
242
  return new Promise((resolve) => {
264
243
  let childProcess = null;
265
244
  const onAbort = () => {
@@ -383,6 +362,7 @@ var ConfigFileSchema = z.object({
383
362
  enableUndo: z.boolean().optional(),
384
363
  enableHookLogDebug: z.boolean().optional(),
385
364
  approvalTimeoutMs: z.number().nonnegative().optional(),
365
+ approvalTimeoutSeconds: z.number().nonnegative().optional(),
386
366
  flightRecorder: z.boolean().optional(),
387
367
  approvers: z.object({
388
368
  native: z.boolean().optional(),
@@ -716,9 +696,9 @@ var SENSITIVE_PATH_PATTERNS = [
716
696
  /[/\\][^/\\]+\.key$/i,
717
697
  /[/\\][^/\\]+\.p12$/i,
718
698
  /[/\\][^/\\]+\.pfx$/i,
719
- /^\/etc\/passwd$/,
720
- /^\/etc\/shadow$/,
721
- /^\/etc\/sudoers$/,
699
+ /^(?:[a-zA-Z]:)?\/etc\/passwd$/,
700
+ /^(?:[a-zA-Z]:)?\/etc\/shadow$/,
701
+ /^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
722
702
  /[/\\]credentials\.json$/i,
723
703
  /[/\\]id_rsa$/i,
724
704
  /[/\\]id_ed25519$/i,
@@ -1119,8 +1099,8 @@ var DEFAULT_CONFIG = {
1119
1099
  enableUndo: true,
1120
1100
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1121
1101
  enableHookLogDebug: true,
1122
- approvalTimeoutMs: 3e4,
1123
- // 30-second auto-deny timeout
1102
+ approvalTimeoutMs: 12e4,
1103
+ // 120-second auto-deny timeout
1124
1104
  flightRecorder: true,
1125
1105
  approvers: { native: true, browser: true, cloud: false, terminal: true }
1126
1106
  },
@@ -1504,14 +1484,12 @@ function getPersistentDecision(toolName) {
1504
1484
  }
1505
1485
  return null;
1506
1486
  }
1507
- async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1487
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
1508
1488
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1509
- const checkCtrl = new AbortController();
1510
- const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
1511
- const onAbort = () => checkCtrl.abort();
1512
- if (signal) signal.addEventListener("abort", onAbort);
1489
+ const ctrl = new AbortController();
1490
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
1513
1491
  try {
1514
- const checkRes = await fetch(`${base}/check`, {
1492
+ const res = await fetch(`${base}/check`, {
1515
1493
  method: "POST",
1516
1494
  headers: { "Content-Type": "application/json" },
1517
1495
  body: JSON.stringify({
@@ -1522,32 +1500,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
1522
1500
  fromCLI: true,
1523
1501
  // Pass the flight-recorder ID so the daemon uses the same UUID for
1524
1502
  // activity-result as the CLI used for the pending activity event.
1525
- // Without this, the two UUIDs never match and tail.ts never resolves
1526
- // the pending item.
1527
1503
  activityId,
1528
- ...riskMetadata && { riskMetadata }
1504
+ ...riskMetadata && { riskMetadata },
1505
+ ...cwd && { cwd }
1529
1506
  }),
1530
- signal: checkCtrl.signal
1507
+ signal: ctrl.signal
1531
1508
  });
1532
- if (!checkRes.ok) throw new Error("Daemon fail");
1533
- const { id } = await checkRes.json();
1534
- const waitCtrl = new AbortController();
1535
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1536
- const onWaitAbort = () => waitCtrl.abort();
1537
- if (signal) signal.addEventListener("abort", onWaitAbort);
1538
- try {
1539
- const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1540
- if (!waitRes.ok) return "deny";
1541
- const { decision } = await waitRes.json();
1542
- if (decision === "allow") return "allow";
1543
- if (decision === "abandoned") return "abandoned";
1544
- return "deny";
1545
- } finally {
1546
- clearTimeout(waitTimer);
1547
- if (signal) signal.removeEventListener("abort", onWaitAbort);
1548
- }
1509
+ if (!res.ok) throw new Error("Daemon fail");
1510
+ const { id } = await res.json();
1511
+ return id;
1549
1512
  } finally {
1550
- clearTimeout(checkTimer);
1513
+ clearTimeout(timer);
1514
+ }
1515
+ }
1516
+ async function waitForDaemonDecision(id, signal) {
1517
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1518
+ const waitCtrl = new AbortController();
1519
+ const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1520
+ const onAbort = () => waitCtrl.abort();
1521
+ if (signal) signal.addEventListener("abort", onAbort);
1522
+ try {
1523
+ const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1524
+ if (!waitRes.ok) return { decision: "deny" };
1525
+ const { decision, source } = await waitRes.json();
1526
+ if (decision === "allow") return { decision: "allow", source };
1527
+ if (decision === "abandoned") return { decision: "abandoned", source };
1528
+ return { decision: "deny", source };
1529
+ } finally {
1530
+ clearTimeout(waitTimer);
1551
1531
  if (signal) signal.removeEventListener("abort", onAbort);
1552
1532
  }
1553
1533
  }
@@ -1595,12 +1575,12 @@ function notifyActivity(data) {
1595
1575
  }
1596
1576
  });
1597
1577
  }
1598
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1578
+ async function authorizeHeadless(toolName, args, meta, options) {
1599
1579
  if (!options?.calledFromDaemon) {
1600
1580
  const actId = randomUUID();
1601
1581
  const actTs = Date.now();
1602
1582
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1603
- const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1583
+ const result = await _authorizeHeadlessCore(toolName, args, meta, {
1604
1584
  ...options,
1605
1585
  activityId: actId
1606
1586
  });
@@ -1615,14 +1595,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1615
1595
  }
1616
1596
  return result;
1617
1597
  }
1618
- return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1598
+ return _authorizeHeadlessCore(toolName, args, meta, options);
1619
1599
  }
1620
- async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1600
+ async function _authorizeHeadlessCore(toolName, args, meta, options) {
1621
1601
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1622
1602
  const pauseState = checkPause();
1623
1603
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1624
1604
  const creds = getCredentials();
1625
- const config = getConfig();
1605
+ const config = getConfig(options?.cwd);
1626
1606
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
1627
1607
  const approvers = {
1628
1608
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -1667,10 +1647,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1667
1647
  if (approvers.cloud && creds?.apiKey) {
1668
1648
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1669
1649
  }
1670
- sendDesktopNotification(
1671
- "Node9 Audit Mode",
1672
- `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
1673
- );
1674
1650
  }
1675
1651
  }
1676
1652
  return { approved: true, checkedBy: "audit" };
@@ -1730,23 +1706,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1730
1706
  return { approved: true };
1731
1707
  }
1732
1708
  let cloudRequestId = null;
1733
- let isRemoteLocked = false;
1734
1709
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
1735
1710
  if (cloudEnforced) {
1736
1711
  try {
1737
1712
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
1738
1713
  if (!initResult.pending) {
1739
1714
  if (initResult.shadowMode) {
1740
- console.error(
1741
- chalk2.yellow(
1742
- `
1743
- \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1744
- )
1745
- );
1746
- if (initResult.shadowReason) {
1747
- console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
1748
- `));
1749
- }
1750
1715
  return { approved: true, checkedBy: "cloud" };
1751
1716
  }
1752
1717
  return {
@@ -1758,36 +1723,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1758
1723
  };
1759
1724
  }
1760
1725
  cloudRequestId = initResult.requestId || null;
1761
- isRemoteLocked = !!initResult.remoteApprovalOnly;
1762
1726
  explainableLabel = "Organization Policy (SaaS)";
1763
- } catch (err) {
1764
- const error = err;
1765
- const isAuthError = error.message.includes("401") || error.message.includes("403");
1766
- const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
1767
- 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;
1768
- console.error(
1769
- chalk2.yellow(`
1770
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
1771
- Falling back to local rules...
1772
- `)
1773
- );
1774
- }
1775
- }
1776
- if (!options?.calledFromDaemon) {
1777
- if (cloudEnforced && cloudRequestId) {
1778
- console.error(
1779
- chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1780
- );
1781
- console.error(
1782
- chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
1783
- );
1784
- } else if (!cloudEnforced) {
1785
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1786
- console.error(
1787
- chalk2.dim(`
1788
- \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
1789
- `)
1790
- );
1727
+ } catch {
1791
1728
  }
1792
1729
  }
1793
1730
  const abortController = new AbortController();
@@ -1814,15 +1751,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1814
1751
  }
1815
1752
  let viewerId = null;
1816
1753
  const internalToken = getInternalToken();
1754
+ let daemonEntryId = null;
1755
+ if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
1756
+ if (cloudEnforced && cloudRequestId) {
1757
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
1758
+ daemonEntryId = viewerId;
1759
+ } else {
1760
+ try {
1761
+ daemonEntryId = await registerDaemonEntry(
1762
+ toolName,
1763
+ args,
1764
+ meta,
1765
+ riskMetadata,
1766
+ options?.activityId,
1767
+ options?.cwd
1768
+ );
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ }
1817
1773
  if (cloudEnforced && cloudRequestId) {
1818
1774
  racePromises.push(
1819
1775
  (async () => {
1820
1776
  try {
1821
- if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1822
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1823
- () => null
1824
- );
1825
- }
1826
1777
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
1827
1778
  return {
1828
1779
  approved: cloudResult.approved,
@@ -1847,7 +1798,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1847
1798
  args,
1848
1799
  meta?.agent,
1849
1800
  explainableLabel,
1850
- isRemoteLocked,
1801
+ false,
1851
1802
  signal,
1852
1803
  policyMatchedField,
1853
1804
  policyMatchedWord
@@ -1862,96 +1813,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1862
1813
  reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
1863
1814
  checkedBy: isApproved ? "daemon" : void 0,
1864
1815
  blockedBy: isApproved ? void 0 : "local-decision",
1865
- blockedByLabel: "User Decision (Native)"
1816
+ blockedByLabel: "User Decision (Native)",
1817
+ decisionSource: "native"
1866
1818
  };
1867
1819
  })()
1868
1820
  );
1869
1821
  }
1870
- if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
1822
+ if (daemonEntryId && (approvers.browser || approvers.terminal)) {
1871
1823
  racePromises.push(
1872
1824
  (async () => {
1873
- try {
1874
- if (!approvers.native && !cloudEnforced) {
1875
- console.error(
1876
- chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
1877
- );
1878
- console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1879
- `));
1880
- }
1881
- const daemonDecision = await askDaemon(
1882
- toolName,
1883
- args,
1884
- meta,
1885
- signal,
1886
- riskMetadata,
1887
- options?.activityId
1888
- );
1889
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
1890
- const isApproved = daemonDecision === "allow";
1891
- return {
1892
- approved: isApproved,
1893
- reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
1894
- checkedBy: isApproved ? "daemon" : void 0,
1895
- blockedBy: isApproved ? void 0 : "local-decision",
1896
- blockedByLabel: "User Decision (Browser)"
1897
- };
1898
- } catch (err) {
1899
- throw err;
1900
- }
1901
- })()
1902
- );
1903
- }
1904
- if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
1905
- racePromises.push(
1906
- (async () => {
1907
- try {
1908
- if (explainableLabel.includes("DLP")) {
1909
- console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1910
- console.log(
1911
- chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
1912
- );
1913
- } else {
1914
- console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1915
- }
1916
- console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
1917
- console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
1918
- if (isRemoteLocked) {
1919
- console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
1920
- `));
1921
- await new Promise((_, reject) => {
1922
- signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
1923
- });
1924
- }
1925
- const TIMEOUT_MS = 6e4;
1926
- let timer;
1927
- const result = await new Promise((resolve, reject) => {
1928
- timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
1929
- confirm(
1930
- { message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
1931
- { signal }
1932
- ).then(resolve).catch(reject);
1933
- });
1934
- clearTimeout(timer);
1935
- return {
1936
- approved: result,
1937
- reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
1938
- checkedBy: result ? "terminal" : void 0,
1939
- blockedBy: result ? void 0 : "local-decision",
1940
- blockedByLabel: "User Decision (Terminal)"
1941
- };
1942
- } catch (err) {
1943
- const error = err;
1944
- if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
1945
- throw err;
1946
- if (error.message === "Terminal Timeout") {
1947
- return {
1948
- approved: false,
1949
- reason: "The terminal prompt timed out without a human response.",
1950
- blockedBy: "local-decision"
1951
- };
1952
- }
1953
- throw err;
1954
- }
1825
+ const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
1826
+ daemonEntryId,
1827
+ signal
1828
+ );
1829
+ if (daemonDecision === "abandoned") throw new Error("Abandoned");
1830
+ const isApproved = daemonDecision === "allow";
1831
+ const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
1832
+ const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
1833
+ return {
1834
+ approved: isApproved,
1835
+ reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
1836
+ checkedBy: isApproved ? "daemon" : void 0,
1837
+ blockedBy: isApproved ? void 0 : "local-decision",
1838
+ blockedByLabel: `User Decision (${via})`,
1839
+ decisionSource: src
1840
+ };
1955
1841
  })()
1956
1842
  );
1957
1843
  }
@@ -2002,7 +1888,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2002
1888
  }
2003
1889
  });
2004
1890
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
2005
- await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
1891
+ await resolveNode9SaaS(
1892
+ cloudRequestId,
1893
+ creds,
1894
+ finalResult.approved,
1895
+ finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
1896
+ );
2006
1897
  }
2007
1898
  if (!isManual) {
2008
1899
  appendLocalAudit(
@@ -2050,6 +1941,8 @@ function getConfig(cwd) {
2050
1941
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
2051
1942
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
2052
1943
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1944
+ if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
1945
+ mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
2053
1946
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
2054
1947
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
2055
1948
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -2209,7 +2102,7 @@ function getCredentials() {
2209
2102
  return null;
2210
2103
  }
2211
2104
  async function authorizeAction(toolName, args) {
2212
- const result = await authorizeHeadless(toolName, args, true);
2105
+ const result = await authorizeHeadless(toolName, args);
2213
2106
  return result.approved;
2214
2107
  }
2215
2108
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
@@ -2278,11 +2171,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
2278
2171
  if (!statusRes.ok) continue;
2279
2172
  const { status, reason } = await statusRes.json();
2280
2173
  if (status === "APPROVED") {
2281
- console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
2282
2174
  return { approved: true, reason };
2283
2175
  }
2284
2176
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
2285
- console.error(chalk2.red("\u274C Denied via Cloud.\n"));
2286
2177
  return { approved: false, reason };
2287
2178
  }
2288
2179
  } catch {
@@ -2290,19 +2181,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
2290
2181
  }
2291
2182
  return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
2292
2183
  }
2293
- async function resolveNode9SaaS(requestId, creds, approved) {
2184
+ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2294
2185
  try {
2295
2186
  const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
2296
2187
  const ctrl = new AbortController();
2297
2188
  const timer = setTimeout(() => ctrl.abort(), 5e3);
2298
- await fetch(resolveUrl, {
2189
+ const res = await fetch(resolveUrl, {
2299
2190
  method: "PATCH",
2300
2191
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
2301
- body: JSON.stringify({ decision: approved ? "APPROVED" : "DENIED" }),
2192
+ body: JSON.stringify({
2193
+ decision: approved ? "APPROVED" : "DENIED",
2194
+ ...decidedBy && { decidedBy }
2195
+ }),
2302
2196
  signal: ctrl.signal
2303
2197
  });
2304
2198
  clearTimeout(timer);
2305
- } catch {
2199
+ if (!res.ok) {
2200
+ fs3.appendFileSync(
2201
+ HOOK_DEBUG_LOG,
2202
+ `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2203
+ `
2204
+ );
2205
+ }
2206
+ } catch (err) {
2207
+ fs3.appendFileSync(
2208
+ HOOK_DEBUG_LOG,
2209
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2210
+ `
2211
+ );
2306
2212
  }
2307
2213
  }
2308
2214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
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",
@@ -61,9 +61,10 @@
61
61
  "test:e2e": "NODE9_TESTING=1 bash scripts/e2e.sh",
62
62
  "preuninstall": "node9 uninstall || echo 'node9 uninstall failed — remove hooks manually from ~/.claude/settings.json'",
63
63
  "prepublishOnly": "npm run validate",
64
- "test": "NODE_ENV=test vitest --run",
65
- "test:watch": "NODE_ENV=test vitest",
66
- "test:ui": "NODE_ENV=test vitest --ui"
64
+ "test": "vitest --run",
65
+ "test:watch": "vitest",
66
+ "test:ui": "vitest --ui",
67
+ "dev:tail": "node -e \"try{const d=JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.node9/daemon.pid','utf8'));process.kill(d.pid)}catch(e){if(e.code!=='ESRCH'&&e.code!=='ENOENT')process.stderr.write(e.message+'\\n')}\" && npm run build && node dist/cli.js tail"
67
68
  },
68
69
  "dependencies": {
69
70
  "@inquirer/prompts": "^8.3.0",