@node9/proxy 2.34.0 → 2.35.0
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 +1023 -504
- package/dist/cli.mjs +998 -479
- package/dist/dashboard.mjs +36 -6
- package/dist/index.js +424 -133
- package/dist/index.mjs +424 -133
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -12934,6 +12934,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
12934
12934
|
...transcriptPathField,
|
|
12935
12935
|
...taintFields,
|
|
12936
12936
|
...exposureFields,
|
|
12937
|
+
...meta?.decidedBy && { decidedBy: meta.decidedBy },
|
|
12938
|
+
...meta?.channel && { channel: meta.channel },
|
|
12939
|
+
...meta?.cardId && { cardId: meta.cardId },
|
|
12937
12940
|
...testRun,
|
|
12938
12941
|
agent: meta?.agent,
|
|
12939
12942
|
mcpServer: meta?.mcpServer,
|
|
@@ -15310,9 +15313,159 @@ function getPersistentDecision(toolName) {
|
|
|
15310
15313
|
|
|
15311
15314
|
// src/auth/daemon.ts
|
|
15312
15315
|
import fs10 from "fs";
|
|
15316
|
+
import http from "http";
|
|
15313
15317
|
import net from "net";
|
|
15314
15318
|
import path10 from "path";
|
|
15315
15319
|
import os8 from "os";
|
|
15320
|
+
|
|
15321
|
+
// src/auth/decision-record.ts
|
|
15322
|
+
var CHANNELS = ["terminal", "monitor", "native", "cloud"];
|
|
15323
|
+
var PERSON_CHANNELS = /* @__PURE__ */ new Set([
|
|
15324
|
+
"terminal",
|
|
15325
|
+
"monitor",
|
|
15326
|
+
"native"
|
|
15327
|
+
]);
|
|
15328
|
+
var CARD_PROTOCOL = 2;
|
|
15329
|
+
var PERSON_LABELS = {
|
|
15330
|
+
terminal: "User Decision (Terminal (node9 tail))",
|
|
15331
|
+
monitor: "User Decision (node9 monitor)",
|
|
15332
|
+
native: "User Decision (Native)"
|
|
15333
|
+
};
|
|
15334
|
+
var REDIRECT_LABEL = "Steered Redirect (Terminal)";
|
|
15335
|
+
var TIMER_LABEL = "Approval Timeout";
|
|
15336
|
+
var CLOUD_LABEL = "Organization Policy (SaaS)";
|
|
15337
|
+
var ABANDONED_LABEL = "No Approver (node9 tail closed)";
|
|
15338
|
+
function personRecord(channel, decision, extra = {}, now = Date.now()) {
|
|
15339
|
+
return {
|
|
15340
|
+
decision,
|
|
15341
|
+
decidedBy: "person",
|
|
15342
|
+
channel,
|
|
15343
|
+
label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
|
|
15344
|
+
...extra,
|
|
15345
|
+
decidedAt: now
|
|
15346
|
+
};
|
|
15347
|
+
}
|
|
15348
|
+
function timerRecord(timeoutMs, now = Date.now()) {
|
|
15349
|
+
return {
|
|
15350
|
+
decision: "deny",
|
|
15351
|
+
decidedBy: "timer",
|
|
15352
|
+
label: TIMER_LABEL,
|
|
15353
|
+
reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
|
|
15354
|
+
decidedAt: now
|
|
15355
|
+
};
|
|
15356
|
+
}
|
|
15357
|
+
function abandonedRecord(now = Date.now()) {
|
|
15358
|
+
return {
|
|
15359
|
+
decision: "deny",
|
|
15360
|
+
decidedBy: "abandoned",
|
|
15361
|
+
label: ABANDONED_LABEL,
|
|
15362
|
+
reason: "No approver was left to answer: node9 tail was closed.",
|
|
15363
|
+
decidedAt: now
|
|
15364
|
+
};
|
|
15365
|
+
}
|
|
15366
|
+
var DECIDED_BY = /* @__PURE__ */ new Set([
|
|
15367
|
+
"person",
|
|
15368
|
+
"timer",
|
|
15369
|
+
"cloud",
|
|
15370
|
+
"policy",
|
|
15371
|
+
"abandoned"
|
|
15372
|
+
]);
|
|
15373
|
+
var STANDING = /* @__PURE__ */ new Set([
|
|
15374
|
+
"always-allow",
|
|
15375
|
+
"always-deny",
|
|
15376
|
+
"trust",
|
|
15377
|
+
"trust-recipients"
|
|
15378
|
+
]);
|
|
15379
|
+
function boundedString(v, max) {
|
|
15380
|
+
return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
|
|
15381
|
+
}
|
|
15382
|
+
function parseDecisionRecord(x) {
|
|
15383
|
+
if (!x || typeof x !== "object") return null;
|
|
15384
|
+
const r = x;
|
|
15385
|
+
if (r.decision !== "allow" && r.decision !== "deny") return null;
|
|
15386
|
+
if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
|
|
15387
|
+
const label2 = boundedString(r.label, 200);
|
|
15388
|
+
if (!label2) return null;
|
|
15389
|
+
const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
|
|
15390
|
+
if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
|
|
15391
|
+
const out = {
|
|
15392
|
+
decision: r.decision,
|
|
15393
|
+
decidedBy: r.decidedBy,
|
|
15394
|
+
label: label2,
|
|
15395
|
+
decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
|
|
15396
|
+
};
|
|
15397
|
+
if (channel) out.channel = channel;
|
|
15398
|
+
const redirect = boundedString(r.redirect, 2e3);
|
|
15399
|
+
if (redirect) out.redirect = redirect;
|
|
15400
|
+
const reason = boundedString(r.reason, 2e3);
|
|
15401
|
+
if (reason) out.reason = reason;
|
|
15402
|
+
const ruleName = boundedString(r.ruleName, 256);
|
|
15403
|
+
if (ruleName) out.ruleName = ruleName;
|
|
15404
|
+
if (typeof r.standing === "string" && STANDING.has(r.standing))
|
|
15405
|
+
out.standing = r.standing;
|
|
15406
|
+
const trustDuration = boundedString(r.trustDuration, 8);
|
|
15407
|
+
if (trustDuration) out.trustDuration = trustDuration;
|
|
15408
|
+
return out;
|
|
15409
|
+
}
|
|
15410
|
+
function legacyWaitFields(record) {
|
|
15411
|
+
switch (record.decidedBy) {
|
|
15412
|
+
case "person": {
|
|
15413
|
+
const source = record.redirect ? "terminal-redirect" : record.channel === "native" ? "native" : "terminal";
|
|
15414
|
+
const reason = record.redirect ?? record.reason;
|
|
15415
|
+
return { decision: record.decision, source, ...reason && { reason } };
|
|
15416
|
+
}
|
|
15417
|
+
case "timer":
|
|
15418
|
+
return {
|
|
15419
|
+
decision: record.decision,
|
|
15420
|
+
source: "timeout",
|
|
15421
|
+
...record.reason && { reason: record.reason }
|
|
15422
|
+
};
|
|
15423
|
+
case "cloud":
|
|
15424
|
+
case "policy":
|
|
15425
|
+
return {
|
|
15426
|
+
decision: record.decision,
|
|
15427
|
+
source: record.decidedBy,
|
|
15428
|
+
...record.reason && { reason: record.reason }
|
|
15429
|
+
};
|
|
15430
|
+
case "abandoned":
|
|
15431
|
+
return { decision: "abandoned" };
|
|
15432
|
+
}
|
|
15433
|
+
}
|
|
15434
|
+
function recordFromLegacyWait(body, now = Date.now()) {
|
|
15435
|
+
if (body.decision === "abandoned") return abandonedRecord(now);
|
|
15436
|
+
const decision = body.decision;
|
|
15437
|
+
switch (body.source) {
|
|
15438
|
+
case "terminal":
|
|
15439
|
+
case "browser":
|
|
15440
|
+
return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
|
|
15441
|
+
case "native":
|
|
15442
|
+
return personRecord("native", decision, {}, now);
|
|
15443
|
+
case "terminal-redirect":
|
|
15444
|
+
return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
|
|
15445
|
+
case "timeout":
|
|
15446
|
+
return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
|
|
15447
|
+
case "cloud":
|
|
15448
|
+
return {
|
|
15449
|
+
decision,
|
|
15450
|
+
decidedBy: "cloud",
|
|
15451
|
+
label: CLOUD_LABEL,
|
|
15452
|
+
...body.reason && { reason: body.reason },
|
|
15453
|
+
decidedAt: now
|
|
15454
|
+
};
|
|
15455
|
+
case "policy":
|
|
15456
|
+
return {
|
|
15457
|
+
decision,
|
|
15458
|
+
decidedBy: "policy",
|
|
15459
|
+
label: "Policy",
|
|
15460
|
+
...body.reason && { reason: body.reason },
|
|
15461
|
+
decidedAt: now
|
|
15462
|
+
};
|
|
15463
|
+
default:
|
|
15464
|
+
return null;
|
|
15465
|
+
}
|
|
15466
|
+
}
|
|
15467
|
+
|
|
15468
|
+
// src/auth/daemon.ts
|
|
15316
15469
|
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
|
|
15317
15470
|
function notifyActivitySocket(data) {
|
|
15318
15471
|
return new Promise((resolve) => {
|
|
@@ -15410,7 +15563,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
15410
15563
|
return false;
|
|
15411
15564
|
}
|
|
15412
15565
|
}
|
|
15413
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
15566
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
|
|
15414
15567
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15415
15568
|
const ctrl = new AbortController();
|
|
15416
15569
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -15436,19 +15589,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
15436
15589
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
15437
15590
|
...viewOnly && { viewOnly: true },
|
|
15438
15591
|
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
15439
|
-
...sessionId && { sessionId }
|
|
15592
|
+
...sessionId && { sessionId },
|
|
15593
|
+
...cardFields(card)
|
|
15440
15594
|
}),
|
|
15441
15595
|
signal: ctrl.signal
|
|
15442
15596
|
});
|
|
15443
15597
|
if (!res.ok) throw new Error("Daemon fail");
|
|
15444
|
-
|
|
15445
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
15598
|
+
return registeredCard(await res.json());
|
|
15446
15599
|
} finally {
|
|
15447
15600
|
clearTimeout(timer);
|
|
15448
15601
|
}
|
|
15449
15602
|
}
|
|
15603
|
+
function cardFields(card) {
|
|
15604
|
+
return {
|
|
15605
|
+
// A review card: the hook already decided this call needs a person; the
|
|
15606
|
+
// daemon never checks it again (approval-card design, defect 1).
|
|
15607
|
+
kind: "review",
|
|
15608
|
+
// Protocol 2 makes the card's /wait require the internal token: claim it
|
|
15609
|
+
// only when this process can read the token (a daemon under another uid
|
|
15610
|
+
// is running but its pid file probe fails with EPERM), or every wait
|
|
15611
|
+
// would get 403 and the review would be denied unseen.
|
|
15612
|
+
...getInternalToken() && { protocol: CARD_PROTOCOL },
|
|
15613
|
+
...card?.channels && { channels: card.channels },
|
|
15614
|
+
...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
|
|
15615
|
+
};
|
|
15616
|
+
}
|
|
15617
|
+
function registeredCard(body) {
|
|
15618
|
+
const b = body;
|
|
15619
|
+
return {
|
|
15620
|
+
id: b.id,
|
|
15621
|
+
allowCount: b.allowCount ?? 1,
|
|
15622
|
+
...b.protocol === CARD_PROTOCOL && {
|
|
15623
|
+
protocol: CARD_PROTOCOL,
|
|
15624
|
+
deadline: typeof b.deadline === "number" ? b.deadline : null
|
|
15625
|
+
}
|
|
15626
|
+
};
|
|
15627
|
+
}
|
|
15450
15628
|
async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
15451
|
-
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15452
15629
|
const waitCtrl = new AbortController();
|
|
15453
15630
|
let capped = false;
|
|
15454
15631
|
const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
|
|
@@ -15458,14 +15635,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15458
15635
|
const onAbort = () => waitCtrl.abort();
|
|
15459
15636
|
if (signal) signal.addEventListener("abort", onAbort);
|
|
15460
15637
|
try {
|
|
15461
|
-
const
|
|
15638
|
+
const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
|
|
15462
15639
|
(err) => {
|
|
15463
15640
|
if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
|
|
15464
15641
|
throw err;
|
|
15465
15642
|
}
|
|
15466
15643
|
);
|
|
15467
|
-
if (
|
|
15468
|
-
|
|
15644
|
+
if (status === 409) throw new Error("node9 card was withdrawn");
|
|
15645
|
+
if (status !== 200) return { decision: "deny" };
|
|
15646
|
+
const parsed = JSON.parse(body);
|
|
15647
|
+
const record = parseDecisionRecord(parsed.record);
|
|
15648
|
+
if (record) return { ...legacyWaitFields(record), record };
|
|
15649
|
+
const { decision, source, reason } = parsed;
|
|
15469
15650
|
if (decision === "allow") return { decision: "allow", source };
|
|
15470
15651
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
15471
15652
|
return { decision: "deny", source, reason };
|
|
@@ -15474,7 +15655,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15474
15655
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
15475
15656
|
}
|
|
15476
15657
|
}
|
|
15477
|
-
|
|
15658
|
+
function daemonGet(urlPath, signal, token = getInternalToken()) {
|
|
15659
|
+
return new Promise((resolve, reject) => {
|
|
15660
|
+
const req = http.get(
|
|
15661
|
+
{
|
|
15662
|
+
host: DAEMON_HOST,
|
|
15663
|
+
port: DAEMON_PORT,
|
|
15664
|
+
path: urlPath,
|
|
15665
|
+
signal,
|
|
15666
|
+
agent: false,
|
|
15667
|
+
headers: token ? { "X-Node9-Internal": token } : {}
|
|
15668
|
+
},
|
|
15669
|
+
(res) => {
|
|
15670
|
+
let body = "";
|
|
15671
|
+
res.setEncoding("utf8");
|
|
15672
|
+
res.on("data", (chunk) => body += chunk);
|
|
15673
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
15674
|
+
res.on("error", reject);
|
|
15675
|
+
}
|
|
15676
|
+
);
|
|
15677
|
+
req.on("error", reject);
|
|
15678
|
+
});
|
|
15679
|
+
}
|
|
15680
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
|
|
15478
15681
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15479
15682
|
const res = await fetch(`${base2}/check`, {
|
|
15480
15683
|
method: "POST",
|
|
@@ -15490,13 +15693,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
|
|
|
15490
15693
|
// event via socket. Same contract as registerDaemonEntry — without
|
|
15491
15694
|
// it the daemon double-emits 'activity' for cloud-enforced flows.
|
|
15492
15695
|
fromCLI: socketActivitySent !== false,
|
|
15493
|
-
activityId
|
|
15696
|
+
activityId,
|
|
15697
|
+
...extra?.cwd && { cwd: extra.cwd },
|
|
15698
|
+
...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
|
|
15699
|
+
...extra?.sessionId && { sessionId: extra.sessionId },
|
|
15700
|
+
...cardFields(extra?.card)
|
|
15494
15701
|
}),
|
|
15495
15702
|
signal: AbortSignal.timeout(3e3)
|
|
15496
15703
|
});
|
|
15497
15704
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
15498
|
-
|
|
15499
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
15705
|
+
return registeredCard(await res.json());
|
|
15500
15706
|
}
|
|
15501
15707
|
async function notifyTaint(filePath, source, fromEid) {
|
|
15502
15708
|
if (!isDaemonRunning()) return;
|
|
@@ -15589,14 +15795,18 @@ async function approveSessionExposure(sessionId, keys) {
|
|
|
15589
15795
|
} catch {
|
|
15590
15796
|
}
|
|
15591
15797
|
}
|
|
15592
|
-
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
15798
|
+
async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
15593
15799
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15594
|
-
await fetch(`${base2}/resolve/${id}`, {
|
|
15800
|
+
const res = await fetch(`${base2}/resolve/${id}`, {
|
|
15595
15801
|
method: "POST",
|
|
15596
15802
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
15597
|
-
body: JSON.stringify({ decision, ...source && { source } }),
|
|
15803
|
+
body: JSON.stringify({ decision, ...source && { source }, ...record && { record } }),
|
|
15598
15804
|
signal: AbortSignal.timeout(3e3)
|
|
15599
15805
|
});
|
|
15806
|
+
if (!res.ok) return null;
|
|
15807
|
+
const body = await res.json().catch(() => null);
|
|
15808
|
+
const winner = parseDecisionRecord(body?.record);
|
|
15809
|
+
return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
|
|
15600
15810
|
}
|
|
15601
15811
|
|
|
15602
15812
|
// src/auth/orchestrator.ts
|
|
@@ -17190,17 +17400,98 @@ function asApprovalRequest(description) {
|
|
|
17190
17400
|
return description;
|
|
17191
17401
|
}
|
|
17192
17402
|
|
|
17193
|
-
// src/auth/
|
|
17194
|
-
var
|
|
17195
|
-
|
|
17196
|
-
"
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
...
|
|
17202
|
-
|
|
17203
|
-
|
|
17403
|
+
// src/auth/card-result.ts
|
|
17404
|
+
var CHANNEL_NAMES = {
|
|
17405
|
+
terminal: "Terminal (node9 tail)",
|
|
17406
|
+
monitor: "node9 monitor",
|
|
17407
|
+
native: "system dialog window",
|
|
17408
|
+
cloud: "organization"
|
|
17409
|
+
};
|
|
17410
|
+
function authResultFromRecord(r, ctx = {}) {
|
|
17411
|
+
const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
|
|
17412
|
+
switch (r.decidedBy) {
|
|
17413
|
+
case "person": {
|
|
17414
|
+
const channel = r.channel ?? "terminal";
|
|
17415
|
+
const decisionSource = channel === "native" ? "native" : "terminal";
|
|
17416
|
+
if (r.decision === "allow") {
|
|
17417
|
+
return { approved: true, checkedBy: "daemon", decisionSource, ...who };
|
|
17418
|
+
}
|
|
17419
|
+
return {
|
|
17420
|
+
approved: false,
|
|
17421
|
+
reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
|
|
17422
|
+
blockedBy: "local-decision",
|
|
17423
|
+
blockedByLabel: r.label,
|
|
17424
|
+
decisionSource,
|
|
17425
|
+
...who,
|
|
17426
|
+
...r.redirect && { redirect: r.redirect }
|
|
17427
|
+
};
|
|
17428
|
+
}
|
|
17429
|
+
case "timer":
|
|
17430
|
+
if (r.decision === "deny" && ctx.downgradedBlock)
|
|
17431
|
+
return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
|
|
17432
|
+
return r.decision === "allow" ? { approved: true, ...who } : {
|
|
17433
|
+
approved: false,
|
|
17434
|
+
reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
|
|
17435
|
+
blockedBy: "timeout",
|
|
17436
|
+
blockedByLabel: TIMER_LABEL,
|
|
17437
|
+
...who
|
|
17438
|
+
};
|
|
17439
|
+
case "cloud":
|
|
17440
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
|
|
17441
|
+
approved: false,
|
|
17442
|
+
reason: r.reason || "Action rejected by organization administrator via Slack.",
|
|
17443
|
+
blockedBy: "team-policy",
|
|
17444
|
+
blockedByLabel: CLOUD_LABEL,
|
|
17445
|
+
decisionSource: "cloud",
|
|
17446
|
+
...who
|
|
17447
|
+
};
|
|
17448
|
+
case "policy":
|
|
17449
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
|
|
17450
|
+
approved: false,
|
|
17451
|
+
reason: r.reason || "Action blocked by policy.",
|
|
17452
|
+
blockedBy: "local-config",
|
|
17453
|
+
blockedByLabel: r.label,
|
|
17454
|
+
...r.ruleName && { ruleHit: r.ruleName },
|
|
17455
|
+
...who
|
|
17456
|
+
};
|
|
17457
|
+
case "abandoned":
|
|
17458
|
+
return {
|
|
17459
|
+
approved: false,
|
|
17460
|
+
reason: r.reason || "No approver was left to answer.",
|
|
17461
|
+
blockedBy: "no-approval-mechanism",
|
|
17462
|
+
blockedByLabel: ABANDONED_LABEL,
|
|
17463
|
+
...who
|
|
17464
|
+
};
|
|
17465
|
+
}
|
|
17466
|
+
}
|
|
17467
|
+
function approvalUnavailable(reason) {
|
|
17468
|
+
return {
|
|
17469
|
+
approved: false,
|
|
17470
|
+
reason: reason || "node9 could not get an approval decision for this call.",
|
|
17471
|
+
blockedBy: "no-approval-mechanism",
|
|
17472
|
+
blockedByLabel: "Approval Unavailable"
|
|
17473
|
+
};
|
|
17474
|
+
}
|
|
17475
|
+
function decidedByOf(res) {
|
|
17476
|
+
if (res.decidedBy) return res.decidedBy;
|
|
17477
|
+
if (res.blockedBy === "timeout") return "timer";
|
|
17478
|
+
if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
|
|
17479
|
+
return "policy";
|
|
17480
|
+
}
|
|
17481
|
+
function recordFromAuthResult(res, now = Date.now()) {
|
|
17482
|
+
const decidedBy = decidedByOf(res);
|
|
17483
|
+
const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
|
|
17484
|
+
return {
|
|
17485
|
+
decision: res.approved ? "allow" : "deny",
|
|
17486
|
+
decidedBy,
|
|
17487
|
+
...channel && { channel },
|
|
17488
|
+
label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
|
|
17489
|
+
...res.reason && { reason: res.reason.slice(0, 2e3) },
|
|
17490
|
+
...res.ruleHit && { ruleName: res.ruleHit },
|
|
17491
|
+
...res.redirect && { redirect: res.redirect },
|
|
17492
|
+
decidedAt: now
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17204
17495
|
|
|
17205
17496
|
// src/auth/orchestrator.ts
|
|
17206
17497
|
var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
|
|
@@ -18240,28 +18531,22 @@ ${packageReview}`;
|
|
|
18240
18531
|
const { signal } = abortController;
|
|
18241
18532
|
const racePromises = [];
|
|
18242
18533
|
const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
|
|
18243
|
-
if (approvalTimeoutMs > 0) {
|
|
18244
|
-
racePromises.push(
|
|
18245
|
-
new Promise((resolve, reject) => {
|
|
18246
|
-
const timer = setTimeout(() => {
|
|
18247
|
-
resolve({
|
|
18248
|
-
approved: false,
|
|
18249
|
-
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
18250
|
-
blockedBy: "timeout",
|
|
18251
|
-
blockedByLabel: "Approval Timeout"
|
|
18252
|
-
});
|
|
18253
|
-
}, approvalTimeoutMs);
|
|
18254
|
-
signal.addEventListener("abort", () => {
|
|
18255
|
-
clearTimeout(timer);
|
|
18256
|
-
reject(new Error("Aborted"));
|
|
18257
|
-
});
|
|
18258
|
-
})
|
|
18259
|
-
);
|
|
18260
|
-
}
|
|
18261
18534
|
let viewerId = null;
|
|
18262
18535
|
const internalToken = getInternalToken();
|
|
18263
18536
|
let daemonEntryId = null;
|
|
18264
18537
|
let daemonAllowCount = 1;
|
|
18538
|
+
let cardDeadline;
|
|
18539
|
+
const cardOptions = {
|
|
18540
|
+
channels: [
|
|
18541
|
+
"terminal",
|
|
18542
|
+
"monitor",
|
|
18543
|
+
// Only when the popup can show: a card listing a channel nobody can
|
|
18544
|
+
// use is never abandoned when node9 tail closes, and waits unanswered.
|
|
18545
|
+
...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
|
|
18546
|
+
...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
|
|
18547
|
+
],
|
|
18548
|
+
timeoutMs: approvalTimeoutMs
|
|
18549
|
+
};
|
|
18265
18550
|
if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
18266
18551
|
if (cloudEnforced && cloudRequestId) {
|
|
18267
18552
|
const viewer = await notifyDaemonViewer(
|
|
@@ -18270,11 +18555,20 @@ ${packageReview}`;
|
|
|
18270
18555
|
meta,
|
|
18271
18556
|
riskMetadata,
|
|
18272
18557
|
options?.activityId,
|
|
18273
|
-
options?.socketActivitySent
|
|
18558
|
+
options?.socketActivitySent,
|
|
18559
|
+
{
|
|
18560
|
+
cwd: options?.cwd,
|
|
18561
|
+
recoveryCommand: statefulRecoveryCommand,
|
|
18562
|
+
sessionId: meta?.sessionId,
|
|
18563
|
+
card: cardOptions
|
|
18564
|
+
}
|
|
18274
18565
|
).catch(() => null);
|
|
18275
18566
|
viewerId = viewer?.id ?? null;
|
|
18276
18567
|
daemonEntryId = viewerId;
|
|
18277
|
-
if (viewer)
|
|
18568
|
+
if (viewer) {
|
|
18569
|
+
daemonAllowCount = viewer.allowCount;
|
|
18570
|
+
cardDeadline = viewer.deadline;
|
|
18571
|
+
}
|
|
18278
18572
|
} else {
|
|
18279
18573
|
try {
|
|
18280
18574
|
const entry = await registerDaemonEntry(
|
|
@@ -18298,14 +18592,35 @@ ${packageReview}`;
|
|
|
18298
18592
|
void 0,
|
|
18299
18593
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
18300
18594
|
options?.socketActivitySent,
|
|
18301
|
-
meta?.sessionId
|
|
18595
|
+
meta?.sessionId,
|
|
18596
|
+
cardOptions
|
|
18302
18597
|
);
|
|
18303
18598
|
daemonEntryId = entry.id;
|
|
18304
18599
|
daemonAllowCount = entry.allowCount;
|
|
18600
|
+
cardDeadline = entry.deadline;
|
|
18305
18601
|
} catch {
|
|
18306
18602
|
}
|
|
18307
18603
|
}
|
|
18308
18604
|
}
|
|
18605
|
+
if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
|
|
18606
|
+
racePromises.push(
|
|
18607
|
+
new Promise((resolve, reject) => {
|
|
18608
|
+
const timer = setTimeout(() => {
|
|
18609
|
+
resolve({
|
|
18610
|
+
approved: false,
|
|
18611
|
+
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
18612
|
+
blockedBy: "timeout",
|
|
18613
|
+
blockedByLabel: "Approval Timeout",
|
|
18614
|
+
decidedBy: "timer"
|
|
18615
|
+
});
|
|
18616
|
+
}, approvalTimeoutMs);
|
|
18617
|
+
signal.addEventListener("abort", () => {
|
|
18618
|
+
clearTimeout(timer);
|
|
18619
|
+
reject(new Error("Aborted"));
|
|
18620
|
+
});
|
|
18621
|
+
})
|
|
18622
|
+
);
|
|
18623
|
+
}
|
|
18309
18624
|
if (cloudEnforced && cloudRequestId && cloudMayVote) {
|
|
18310
18625
|
racePromises.push(
|
|
18311
18626
|
(async () => {
|
|
@@ -18316,7 +18631,9 @@ ${packageReview}`;
|
|
|
18316
18631
|
reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
|
|
18317
18632
|
checkedBy: cloudResult.approved ? "cloud" : void 0,
|
|
18318
18633
|
blockedBy: cloudResult.approved ? void 0 : "team-policy",
|
|
18319
|
-
blockedByLabel: "Organization Policy (SaaS)"
|
|
18634
|
+
blockedByLabel: "Organization Policy (SaaS)",
|
|
18635
|
+
decidedBy: "cloud",
|
|
18636
|
+
channel: "cloud"
|
|
18320
18637
|
};
|
|
18321
18638
|
} catch (err) {
|
|
18322
18639
|
const error = err;
|
|
@@ -18346,14 +18663,31 @@ ${packageReview}`;
|
|
|
18346
18663
|
if (config.policySource !== "workspace") {
|
|
18347
18664
|
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
18348
18665
|
}
|
|
18349
|
-
return {
|
|
18666
|
+
return {
|
|
18667
|
+
approved: true,
|
|
18668
|
+
checkedBy: "daemon",
|
|
18669
|
+
decisionSource: "native",
|
|
18670
|
+
decidedBy: "person",
|
|
18671
|
+
channel: "native"
|
|
18672
|
+
};
|
|
18350
18673
|
}
|
|
18351
18674
|
if (decision === "always_allow") {
|
|
18352
18675
|
if (standingTrustApplies()) {
|
|
18353
18676
|
writeTrustSession(toolName, 36e5, args);
|
|
18354
|
-
return {
|
|
18677
|
+
return {
|
|
18678
|
+
approved: true,
|
|
18679
|
+
checkedBy: "trust",
|
|
18680
|
+
decidedBy: "person",
|
|
18681
|
+
channel: "native"
|
|
18682
|
+
};
|
|
18355
18683
|
}
|
|
18356
|
-
return {
|
|
18684
|
+
return {
|
|
18685
|
+
approved: true,
|
|
18686
|
+
checkedBy: "daemon",
|
|
18687
|
+
decisionSource: "native",
|
|
18688
|
+
decidedBy: "person",
|
|
18689
|
+
channel: "native"
|
|
18690
|
+
};
|
|
18357
18691
|
}
|
|
18358
18692
|
const isApproved = decision === "allow";
|
|
18359
18693
|
return {
|
|
@@ -18362,75 +18696,32 @@ ${packageReview}`;
|
|
|
18362
18696
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
18363
18697
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18364
18698
|
blockedByLabel: "User Decision (Native)",
|
|
18365
|
-
decisionSource: "native"
|
|
18699
|
+
decisionSource: "native",
|
|
18700
|
+
decidedBy: "person",
|
|
18701
|
+
channel: "native"
|
|
18366
18702
|
};
|
|
18367
18703
|
})()
|
|
18368
18704
|
);
|
|
18369
18705
|
}
|
|
18706
|
+
const fromCard = /* @__PURE__ */ new WeakSet();
|
|
18707
|
+
const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
|
|
18370
18708
|
if (daemonEntryId && approvers.terminal) {
|
|
18371
18709
|
racePromises.push(
|
|
18372
18710
|
(async () => {
|
|
18373
|
-
const
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
);
|
|
18382
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
18383
|
-
const isApproved = daemonDecision === "allow";
|
|
18384
|
-
const isRedirect = decisionSource === REDIRECT_SOURCE;
|
|
18385
|
-
if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
|
|
18386
|
-
if (decisionSource === "cloud")
|
|
18387
|
-
return {
|
|
18388
|
-
approved: false,
|
|
18389
|
-
reason: daemonReason || "Action rejected by organization administrator via Slack.",
|
|
18390
|
-
blockedBy: "team-policy",
|
|
18391
|
-
blockedByLabel: "Organization Policy (SaaS)",
|
|
18392
|
-
decisionSource: "cloud"
|
|
18393
|
-
};
|
|
18394
|
-
const timedOut = decisionSource === "timeout";
|
|
18395
|
-
if (downgradedBlock)
|
|
18396
|
-
return {
|
|
18397
|
-
...downgradedBlock,
|
|
18398
|
-
blockedBy: timedOut ? "timeout" : "local-config"
|
|
18399
|
-
};
|
|
18400
|
-
if (timedOut)
|
|
18401
|
-
return {
|
|
18402
|
-
approved: false,
|
|
18403
|
-
reason: daemonReason || "No response: auto-denied after the approval timeout.",
|
|
18404
|
-
blockedBy: "timeout",
|
|
18405
|
-
blockedByLabel: "Approval Timeout"
|
|
18406
|
-
};
|
|
18407
|
-
if (decisionSource === "policy")
|
|
18408
|
-
return {
|
|
18409
|
-
approved: false,
|
|
18410
|
-
reason: daemonReason || "Action blocked by policy.",
|
|
18411
|
-
blockedBy: "local-config",
|
|
18412
|
-
blockedByLabel: explainableLabel
|
|
18413
|
-
};
|
|
18414
|
-
return {
|
|
18415
|
-
approved: false,
|
|
18416
|
-
reason: daemonReason || "node9 could not get an approval decision for this call.",
|
|
18417
|
-
blockedBy: "no-approval-mechanism",
|
|
18418
|
-
blockedByLabel: "Approval Unavailable"
|
|
18419
|
-
};
|
|
18711
|
+
const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
|
|
18712
|
+
const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
|
|
18713
|
+
const legacy = !outcome.record;
|
|
18714
|
+
const record = outcome.record ?? recordFromLegacyWait(outcome);
|
|
18715
|
+
if (downgradedBlock && (!record || legacy && record.decidedBy === "policy" && record.decision === "deny")) {
|
|
18716
|
+
const softened = { ...downgradedBlock, blockedBy: "local-config" };
|
|
18717
|
+
fromCard.add(softened);
|
|
18718
|
+
return softened;
|
|
18420
18719
|
}
|
|
18421
|
-
if (
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
return {
|
|
18427
|
-
approved: isApproved,
|
|
18428
|
-
reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
|
|
18429
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
18430
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18431
|
-
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
18432
|
-
decisionSource: "terminal"
|
|
18433
|
-
};
|
|
18720
|
+
if (!record) return approvalUnavailable(outcome.reason);
|
|
18721
|
+
if (legacy && record.decidedBy === "policy") record.label = explainableLabel;
|
|
18722
|
+
const result = authResultFromRecord(record, { downgradedBlock, timeoutReason });
|
|
18723
|
+
fromCard.add(result);
|
|
18724
|
+
return result;
|
|
18434
18725
|
})()
|
|
18435
18726
|
);
|
|
18436
18727
|
}
|
|
@@ -18462,7 +18753,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18462
18753
|
blockedByLabel: explainableLabel
|
|
18463
18754
|
};
|
|
18464
18755
|
}
|
|
18465
|
-
const
|
|
18756
|
+
const raceResult = await new Promise((resolve) => {
|
|
18466
18757
|
let resolved = false;
|
|
18467
18758
|
let failures = 0;
|
|
18468
18759
|
const total = racePromises.length;
|
|
@@ -18470,14 +18761,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18470
18761
|
if (!resolved) {
|
|
18471
18762
|
resolved = true;
|
|
18472
18763
|
abortController.abort();
|
|
18473
|
-
if (daemonEntryId && internalToken) {
|
|
18474
|
-
resolveViaDaemon(
|
|
18475
|
-
daemonEntryId,
|
|
18476
|
-
res.approved ? "allow" : "deny",
|
|
18477
|
-
internalToken,
|
|
18478
|
-
res.decisionSource
|
|
18479
|
-
).catch(() => null);
|
|
18480
|
-
}
|
|
18481
18764
|
resolve(res);
|
|
18482
18765
|
}
|
|
18483
18766
|
};
|
|
@@ -18485,26 +18768,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18485
18768
|
p.then(finish).catch((err) => {
|
|
18486
18769
|
if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
|
|
18487
18770
|
return;
|
|
18488
|
-
if (err.message === "Abandoned") {
|
|
18489
|
-
finish({
|
|
18490
|
-
approved: false,
|
|
18491
|
-
reason: "Browser dashboard closed without making a decision.",
|
|
18492
|
-
blockedBy: "local-decision",
|
|
18493
|
-
blockedByLabel: "Browser Dashboard (Abandoned)"
|
|
18494
|
-
});
|
|
18495
|
-
return;
|
|
18496
|
-
}
|
|
18497
18771
|
failures++;
|
|
18498
18772
|
if (failures === total && !resolved) {
|
|
18499
|
-
finish(
|
|
18773
|
+
finish(approvalUnavailable("All approval channels failed or disconnected."));
|
|
18500
18774
|
}
|
|
18501
18775
|
});
|
|
18502
18776
|
}
|
|
18503
18777
|
});
|
|
18778
|
+
let finalResult = raceResult;
|
|
18779
|
+
if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
|
|
18780
|
+
const reply = await resolveViaDaemon(
|
|
18781
|
+
daemonEntryId,
|
|
18782
|
+
raceResult.approved ? "allow" : "deny",
|
|
18783
|
+
internalToken,
|
|
18784
|
+
raceResult.decisionSource,
|
|
18785
|
+
recordFromAuthResult(raceResult)
|
|
18786
|
+
).catch(() => null);
|
|
18787
|
+
if (reply && !reply.won)
|
|
18788
|
+
finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
|
|
18789
|
+
}
|
|
18504
18790
|
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
18505
18791
|
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
18506
18792
|
}
|
|
18507
|
-
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
18793
|
+
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
|
|
18508
18794
|
await resolveNode9SaaS(
|
|
18509
18795
|
cloudRequestId,
|
|
18510
18796
|
creds,
|
|
@@ -18528,7 +18814,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18528
18814
|
{
|
|
18529
18815
|
...ownRowMeta(finalResult.approved),
|
|
18530
18816
|
...cloudRequestId && { cloudRequestId },
|
|
18531
|
-
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
|
|
18817
|
+
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
|
|
18818
|
+
// Who decided, from the decision record, and the card: the daemon
|
|
18819
|
+
// writes its own row for the same card, and the report counts one.
|
|
18820
|
+
...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
|
|
18821
|
+
...finalResult.channel && { channel: finalResult.channel },
|
|
18822
|
+
...daemonEntryId && { cardId: daemonEntryId }
|
|
18532
18823
|
},
|
|
18533
18824
|
hashAuditArgs
|
|
18534
18825
|
);
|