@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.js CHANGED
@@ -35,8 +35,6 @@ __export(src_exports, {
35
35
  module.exports = __toCommonJS(src_exports);
36
36
 
37
37
  // src/core.ts
38
- var import_chalk2 = __toESM(require("chalk"));
39
- var import_prompts = require("@inquirer/prompts");
40
38
  var import_fs3 = __toESM(require("fs"));
41
39
  var import_path5 = __toESM(require("path"));
42
40
  var import_os2 = __toESM(require("os"));
@@ -50,7 +48,6 @@ var import_sh_syntax = require("sh-syntax");
50
48
  // src/ui/native.ts
51
49
  var import_child_process = require("child_process");
52
50
  var import_path2 = __toESM(require("path"));
53
- var import_chalk = __toESM(require("chalk"));
54
51
 
55
52
  // src/context-sniper.ts
56
53
  var import_path = __toESM(require("path"));
@@ -236,21 +233,6 @@ ${smartTruncate(str, 500)}`
236
233
  }
237
234
  return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
238
235
  }
239
- function sendDesktopNotification(title, body) {
240
- if (isTestEnv()) return;
241
- try {
242
- if (process.platform === "darwin") {
243
- const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
244
- (0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
245
- } else if (process.platform === "linux") {
246
- (0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
247
- detached: true,
248
- stdio: "ignore"
249
- }).unref();
250
- }
251
- } catch {
252
- }
253
- }
254
236
  function escapePango(text) {
255
237
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
256
238
  }
@@ -293,9 +275,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
293
275
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
294
276
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
295
277
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
296
- process.stderr.write(import_chalk.default.yellow(`
297
- \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
298
- `));
299
278
  return new Promise((resolve) => {
300
279
  let childProcess = null;
301
280
  const onAbort = () => {
@@ -419,6 +398,7 @@ var ConfigFileSchema = import_zod.z.object({
419
398
  enableUndo: import_zod.z.boolean().optional(),
420
399
  enableHookLogDebug: import_zod.z.boolean().optional(),
421
400
  approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
401
+ approvalTimeoutSeconds: import_zod.z.number().nonnegative().optional(),
422
402
  flightRecorder: import_zod.z.boolean().optional(),
423
403
  approvers: import_zod.z.object({
424
404
  native: import_zod.z.boolean().optional(),
@@ -752,9 +732,9 @@ var SENSITIVE_PATH_PATTERNS = [
752
732
  /[/\\][^/\\]+\.key$/i,
753
733
  /[/\\][^/\\]+\.p12$/i,
754
734
  /[/\\][^/\\]+\.pfx$/i,
755
- /^\/etc\/passwd$/,
756
- /^\/etc\/shadow$/,
757
- /^\/etc\/sudoers$/,
735
+ /^(?:[a-zA-Z]:)?\/etc\/passwd$/,
736
+ /^(?:[a-zA-Z]:)?\/etc\/shadow$/,
737
+ /^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
758
738
  /[/\\]credentials\.json$/i,
759
739
  /[/\\]id_rsa$/i,
760
740
  /[/\\]id_ed25519$/i,
@@ -1155,8 +1135,8 @@ var DEFAULT_CONFIG = {
1155
1135
  enableUndo: true,
1156
1136
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1157
1137
  enableHookLogDebug: true,
1158
- approvalTimeoutMs: 3e4,
1159
- // 30-second auto-deny timeout
1138
+ approvalTimeoutMs: 12e4,
1139
+ // 120-second auto-deny timeout
1160
1140
  flightRecorder: true,
1161
1141
  approvers: { native: true, browser: true, cloud: false, terminal: true }
1162
1142
  },
@@ -1540,14 +1520,12 @@ function getPersistentDecision(toolName) {
1540
1520
  }
1541
1521
  return null;
1542
1522
  }
1543
- async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1523
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
1544
1524
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1545
- const checkCtrl = new AbortController();
1546
- const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
1547
- const onAbort = () => checkCtrl.abort();
1548
- if (signal) signal.addEventListener("abort", onAbort);
1525
+ const ctrl = new AbortController();
1526
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
1549
1527
  try {
1550
- const checkRes = await fetch(`${base}/check`, {
1528
+ const res = await fetch(`${base}/check`, {
1551
1529
  method: "POST",
1552
1530
  headers: { "Content-Type": "application/json" },
1553
1531
  body: JSON.stringify({
@@ -1558,32 +1536,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
1558
1536
  fromCLI: true,
1559
1537
  // Pass the flight-recorder ID so the daemon uses the same UUID for
1560
1538
  // activity-result as the CLI used for the pending activity event.
1561
- // Without this, the two UUIDs never match and tail.ts never resolves
1562
- // the pending item.
1563
1539
  activityId,
1564
- ...riskMetadata && { riskMetadata }
1540
+ ...riskMetadata && { riskMetadata },
1541
+ ...cwd && { cwd }
1565
1542
  }),
1566
- signal: checkCtrl.signal
1543
+ signal: ctrl.signal
1567
1544
  });
1568
- if (!checkRes.ok) throw new Error("Daemon fail");
1569
- const { id } = await checkRes.json();
1570
- const waitCtrl = new AbortController();
1571
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1572
- const onWaitAbort = () => waitCtrl.abort();
1573
- if (signal) signal.addEventListener("abort", onWaitAbort);
1574
- try {
1575
- const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1576
- if (!waitRes.ok) return "deny";
1577
- const { decision } = await waitRes.json();
1578
- if (decision === "allow") return "allow";
1579
- if (decision === "abandoned") return "abandoned";
1580
- return "deny";
1581
- } finally {
1582
- clearTimeout(waitTimer);
1583
- if (signal) signal.removeEventListener("abort", onWaitAbort);
1584
- }
1545
+ if (!res.ok) throw new Error("Daemon fail");
1546
+ const { id } = await res.json();
1547
+ return id;
1585
1548
  } finally {
1586
- clearTimeout(checkTimer);
1549
+ clearTimeout(timer);
1550
+ }
1551
+ }
1552
+ async function waitForDaemonDecision(id, signal) {
1553
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1554
+ const waitCtrl = new AbortController();
1555
+ const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1556
+ const onAbort = () => waitCtrl.abort();
1557
+ if (signal) signal.addEventListener("abort", onAbort);
1558
+ try {
1559
+ const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1560
+ if (!waitRes.ok) return { decision: "deny" };
1561
+ const { decision, source } = await waitRes.json();
1562
+ if (decision === "allow") return { decision: "allow", source };
1563
+ if (decision === "abandoned") return { decision: "abandoned", source };
1564
+ return { decision: "deny", source };
1565
+ } finally {
1566
+ clearTimeout(waitTimer);
1587
1567
  if (signal) signal.removeEventListener("abort", onAbort);
1588
1568
  }
1589
1569
  }
@@ -1631,12 +1611,12 @@ function notifyActivity(data) {
1631
1611
  }
1632
1612
  });
1633
1613
  }
1634
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1614
+ async function authorizeHeadless(toolName, args, meta, options) {
1635
1615
  if (!options?.calledFromDaemon) {
1636
1616
  const actId = (0, import_crypto.randomUUID)();
1637
1617
  const actTs = Date.now();
1638
1618
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1639
- const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1619
+ const result = await _authorizeHeadlessCore(toolName, args, meta, {
1640
1620
  ...options,
1641
1621
  activityId: actId
1642
1622
  });
@@ -1651,14 +1631,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1651
1631
  }
1652
1632
  return result;
1653
1633
  }
1654
- return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1634
+ return _authorizeHeadlessCore(toolName, args, meta, options);
1655
1635
  }
1656
- async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1636
+ async function _authorizeHeadlessCore(toolName, args, meta, options) {
1657
1637
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1658
1638
  const pauseState = checkPause();
1659
1639
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1660
1640
  const creds = getCredentials();
1661
- const config = getConfig();
1641
+ const config = getConfig(options?.cwd);
1662
1642
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
1663
1643
  const approvers = {
1664
1644
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -1703,10 +1683,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1703
1683
  if (approvers.cloud && creds?.apiKey) {
1704
1684
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1705
1685
  }
1706
- sendDesktopNotification(
1707
- "Node9 Audit Mode",
1708
- `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
1709
- );
1710
1686
  }
1711
1687
  }
1712
1688
  return { approved: true, checkedBy: "audit" };
@@ -1766,23 +1742,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1766
1742
  return { approved: true };
1767
1743
  }
1768
1744
  let cloudRequestId = null;
1769
- let isRemoteLocked = false;
1770
1745
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
1771
1746
  if (cloudEnforced) {
1772
1747
  try {
1773
1748
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
1774
1749
  if (!initResult.pending) {
1775
1750
  if (initResult.shadowMode) {
1776
- console.error(
1777
- import_chalk2.default.yellow(
1778
- `
1779
- \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1780
- )
1781
- );
1782
- if (initResult.shadowReason) {
1783
- console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
1784
- `));
1785
- }
1786
1751
  return { approved: true, checkedBy: "cloud" };
1787
1752
  }
1788
1753
  return {
@@ -1794,36 +1759,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1794
1759
  };
1795
1760
  }
1796
1761
  cloudRequestId = initResult.requestId || null;
1797
- isRemoteLocked = !!initResult.remoteApprovalOnly;
1798
1762
  explainableLabel = "Organization Policy (SaaS)";
1799
- } catch (err) {
1800
- const error = err;
1801
- const isAuthError = error.message.includes("401") || error.message.includes("403");
1802
- const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
1803
- 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;
1804
- console.error(
1805
- import_chalk2.default.yellow(`
1806
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
1807
- Falling back to local rules...
1808
- `)
1809
- );
1810
- }
1811
- }
1812
- if (!options?.calledFromDaemon) {
1813
- if (cloudEnforced && cloudRequestId) {
1814
- console.error(
1815
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1816
- );
1817
- console.error(
1818
- import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
1819
- );
1820
- } else if (!cloudEnforced) {
1821
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1822
- console.error(
1823
- import_chalk2.default.dim(`
1824
- \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
1825
- `)
1826
- );
1763
+ } catch {
1827
1764
  }
1828
1765
  }
1829
1766
  const abortController = new AbortController();
@@ -1850,15 +1787,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1850
1787
  }
1851
1788
  let viewerId = null;
1852
1789
  const internalToken = getInternalToken();
1790
+ let daemonEntryId = null;
1791
+ if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
1792
+ if (cloudEnforced && cloudRequestId) {
1793
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
1794
+ daemonEntryId = viewerId;
1795
+ } else {
1796
+ try {
1797
+ daemonEntryId = await registerDaemonEntry(
1798
+ toolName,
1799
+ args,
1800
+ meta,
1801
+ riskMetadata,
1802
+ options?.activityId,
1803
+ options?.cwd
1804
+ );
1805
+ } catch {
1806
+ }
1807
+ }
1808
+ }
1853
1809
  if (cloudEnforced && cloudRequestId) {
1854
1810
  racePromises.push(
1855
1811
  (async () => {
1856
1812
  try {
1857
- if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1858
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1859
- () => null
1860
- );
1861
- }
1862
1813
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
1863
1814
  return {
1864
1815
  approved: cloudResult.approved,
@@ -1883,7 +1834,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1883
1834
  args,
1884
1835
  meta?.agent,
1885
1836
  explainableLabel,
1886
- isRemoteLocked,
1837
+ false,
1887
1838
  signal,
1888
1839
  policyMatchedField,
1889
1840
  policyMatchedWord
@@ -1898,96 +1849,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1898
1849
  reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
1899
1850
  checkedBy: isApproved ? "daemon" : void 0,
1900
1851
  blockedBy: isApproved ? void 0 : "local-decision",
1901
- blockedByLabel: "User Decision (Native)"
1852
+ blockedByLabel: "User Decision (Native)",
1853
+ decisionSource: "native"
1902
1854
  };
1903
1855
  })()
1904
1856
  );
1905
1857
  }
1906
- if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
1858
+ if (daemonEntryId && (approvers.browser || approvers.terminal)) {
1907
1859
  racePromises.push(
1908
1860
  (async () => {
1909
- try {
1910
- if (!approvers.native && !cloudEnforced) {
1911
- console.error(
1912
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
1913
- );
1914
- console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1915
- `));
1916
- }
1917
- const daemonDecision = await askDaemon(
1918
- toolName,
1919
- args,
1920
- meta,
1921
- signal,
1922
- riskMetadata,
1923
- options?.activityId
1924
- );
1925
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
1926
- const isApproved = daemonDecision === "allow";
1927
- return {
1928
- approved: isApproved,
1929
- reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
1930
- checkedBy: isApproved ? "daemon" : void 0,
1931
- blockedBy: isApproved ? void 0 : "local-decision",
1932
- blockedByLabel: "User Decision (Browser)"
1933
- };
1934
- } catch (err) {
1935
- throw err;
1936
- }
1937
- })()
1938
- );
1939
- }
1940
- if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
1941
- racePromises.push(
1942
- (async () => {
1943
- try {
1944
- if (explainableLabel.includes("DLP")) {
1945
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1946
- console.log(
1947
- import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
1948
- );
1949
- } else {
1950
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1951
- }
1952
- console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
1953
- console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
1954
- if (isRemoteLocked) {
1955
- console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
1956
- `));
1957
- await new Promise((_, reject) => {
1958
- signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
1959
- });
1960
- }
1961
- const TIMEOUT_MS = 6e4;
1962
- let timer;
1963
- const result = await new Promise((resolve, reject) => {
1964
- timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
1965
- (0, import_prompts.confirm)(
1966
- { message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
1967
- { signal }
1968
- ).then(resolve).catch(reject);
1969
- });
1970
- clearTimeout(timer);
1971
- return {
1972
- approved: result,
1973
- reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
1974
- checkedBy: result ? "terminal" : void 0,
1975
- blockedBy: result ? void 0 : "local-decision",
1976
- blockedByLabel: "User Decision (Terminal)"
1977
- };
1978
- } catch (err) {
1979
- const error = err;
1980
- if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
1981
- throw err;
1982
- if (error.message === "Terminal Timeout") {
1983
- return {
1984
- approved: false,
1985
- reason: "The terminal prompt timed out without a human response.",
1986
- blockedBy: "local-decision"
1987
- };
1988
- }
1989
- throw err;
1990
- }
1861
+ const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
1862
+ daemonEntryId,
1863
+ signal
1864
+ );
1865
+ if (daemonDecision === "abandoned") throw new Error("Abandoned");
1866
+ const isApproved = daemonDecision === "allow";
1867
+ const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
1868
+ const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
1869
+ return {
1870
+ approved: isApproved,
1871
+ reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
1872
+ checkedBy: isApproved ? "daemon" : void 0,
1873
+ blockedBy: isApproved ? void 0 : "local-decision",
1874
+ blockedByLabel: `User Decision (${via})`,
1875
+ decisionSource: src
1876
+ };
1991
1877
  })()
1992
1878
  );
1993
1879
  }
@@ -2038,7 +1924,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2038
1924
  }
2039
1925
  });
2040
1926
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
2041
- await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
1927
+ await resolveNode9SaaS(
1928
+ cloudRequestId,
1929
+ creds,
1930
+ finalResult.approved,
1931
+ finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
1932
+ );
2042
1933
  }
2043
1934
  if (!isManual) {
2044
1935
  appendLocalAudit(
@@ -2086,6 +1977,8 @@ function getConfig(cwd) {
2086
1977
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
2087
1978
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
2088
1979
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1980
+ if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
1981
+ mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
2089
1982
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
2090
1983
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
2091
1984
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -2245,7 +2138,7 @@ function getCredentials() {
2245
2138
  return null;
2246
2139
  }
2247
2140
  async function authorizeAction(toolName, args) {
2248
- const result = await authorizeHeadless(toolName, args, true);
2141
+ const result = await authorizeHeadless(toolName, args);
2249
2142
  return result.approved;
2250
2143
  }
2251
2144
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
@@ -2314,11 +2207,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
2314
2207
  if (!statusRes.ok) continue;
2315
2208
  const { status, reason } = await statusRes.json();
2316
2209
  if (status === "APPROVED") {
2317
- console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
2318
2210
  return { approved: true, reason };
2319
2211
  }
2320
2212
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
2321
- console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
2322
2213
  return { approved: false, reason };
2323
2214
  }
2324
2215
  } catch {
@@ -2326,19 +2217,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
2326
2217
  }
2327
2218
  return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
2328
2219
  }
2329
- async function resolveNode9SaaS(requestId, creds, approved) {
2220
+ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2330
2221
  try {
2331
2222
  const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
2332
2223
  const ctrl = new AbortController();
2333
2224
  const timer = setTimeout(() => ctrl.abort(), 5e3);
2334
- await fetch(resolveUrl, {
2225
+ const res = await fetch(resolveUrl, {
2335
2226
  method: "PATCH",
2336
2227
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
2337
- body: JSON.stringify({ decision: approved ? "APPROVED" : "DENIED" }),
2228
+ body: JSON.stringify({
2229
+ decision: approved ? "APPROVED" : "DENIED",
2230
+ ...decidedBy && { decidedBy }
2231
+ }),
2338
2232
  signal: ctrl.signal
2339
2233
  });
2340
2234
  clearTimeout(timer);
2341
- } catch {
2235
+ if (!res.ok) {
2236
+ import_fs3.default.appendFileSync(
2237
+ HOOK_DEBUG_LOG,
2238
+ `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2239
+ `
2240
+ );
2241
+ }
2242
+ } catch (err) {
2243
+ import_fs3.default.appendFileSync(
2244
+ HOOK_DEBUG_LOG,
2245
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2246
+ `
2247
+ );
2342
2248
  }
2343
2249
  }
2344
2250