@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.js
CHANGED
|
@@ -12953,6 +12953,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
12953
12953
|
...transcriptPathField,
|
|
12954
12954
|
...taintFields,
|
|
12955
12955
|
...exposureFields,
|
|
12956
|
+
...meta?.decidedBy && { decidedBy: meta.decidedBy },
|
|
12957
|
+
...meta?.channel && { channel: meta.channel },
|
|
12958
|
+
...meta?.cardId && { cardId: meta.cardId },
|
|
12956
12959
|
...testRun,
|
|
12957
12960
|
agent: meta?.agent,
|
|
12958
12961
|
mcpServer: meta?.mcpServer,
|
|
@@ -15340,9 +15343,159 @@ function getPersistentDecision(toolName) {
|
|
|
15340
15343
|
|
|
15341
15344
|
// src/auth/daemon.ts
|
|
15342
15345
|
var import_fs10 = __toESM(require("fs"));
|
|
15346
|
+
var import_http = __toESM(require("http"));
|
|
15343
15347
|
var import_net = __toESM(require("net"));
|
|
15344
15348
|
var import_path10 = __toESM(require("path"));
|
|
15345
15349
|
var import_os8 = __toESM(require("os"));
|
|
15350
|
+
|
|
15351
|
+
// src/auth/decision-record.ts
|
|
15352
|
+
var CHANNELS = ["terminal", "monitor", "native", "cloud"];
|
|
15353
|
+
var PERSON_CHANNELS = /* @__PURE__ */ new Set([
|
|
15354
|
+
"terminal",
|
|
15355
|
+
"monitor",
|
|
15356
|
+
"native"
|
|
15357
|
+
]);
|
|
15358
|
+
var CARD_PROTOCOL = 2;
|
|
15359
|
+
var PERSON_LABELS = {
|
|
15360
|
+
terminal: "User Decision (Terminal (node9 tail))",
|
|
15361
|
+
monitor: "User Decision (node9 monitor)",
|
|
15362
|
+
native: "User Decision (Native)"
|
|
15363
|
+
};
|
|
15364
|
+
var REDIRECT_LABEL = "Steered Redirect (Terminal)";
|
|
15365
|
+
var TIMER_LABEL = "Approval Timeout";
|
|
15366
|
+
var CLOUD_LABEL = "Organization Policy (SaaS)";
|
|
15367
|
+
var ABANDONED_LABEL = "No Approver (node9 tail closed)";
|
|
15368
|
+
function personRecord(channel, decision, extra = {}, now = Date.now()) {
|
|
15369
|
+
return {
|
|
15370
|
+
decision,
|
|
15371
|
+
decidedBy: "person",
|
|
15372
|
+
channel,
|
|
15373
|
+
label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
|
|
15374
|
+
...extra,
|
|
15375
|
+
decidedAt: now
|
|
15376
|
+
};
|
|
15377
|
+
}
|
|
15378
|
+
function timerRecord(timeoutMs, now = Date.now()) {
|
|
15379
|
+
return {
|
|
15380
|
+
decision: "deny",
|
|
15381
|
+
decidedBy: "timer",
|
|
15382
|
+
label: TIMER_LABEL,
|
|
15383
|
+
reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
|
|
15384
|
+
decidedAt: now
|
|
15385
|
+
};
|
|
15386
|
+
}
|
|
15387
|
+
function abandonedRecord(now = Date.now()) {
|
|
15388
|
+
return {
|
|
15389
|
+
decision: "deny",
|
|
15390
|
+
decidedBy: "abandoned",
|
|
15391
|
+
label: ABANDONED_LABEL,
|
|
15392
|
+
reason: "No approver was left to answer: node9 tail was closed.",
|
|
15393
|
+
decidedAt: now
|
|
15394
|
+
};
|
|
15395
|
+
}
|
|
15396
|
+
var DECIDED_BY = /* @__PURE__ */ new Set([
|
|
15397
|
+
"person",
|
|
15398
|
+
"timer",
|
|
15399
|
+
"cloud",
|
|
15400
|
+
"policy",
|
|
15401
|
+
"abandoned"
|
|
15402
|
+
]);
|
|
15403
|
+
var STANDING = /* @__PURE__ */ new Set([
|
|
15404
|
+
"always-allow",
|
|
15405
|
+
"always-deny",
|
|
15406
|
+
"trust",
|
|
15407
|
+
"trust-recipients"
|
|
15408
|
+
]);
|
|
15409
|
+
function boundedString(v, max) {
|
|
15410
|
+
return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
|
|
15411
|
+
}
|
|
15412
|
+
function parseDecisionRecord(x) {
|
|
15413
|
+
if (!x || typeof x !== "object") return null;
|
|
15414
|
+
const r = x;
|
|
15415
|
+
if (r.decision !== "allow" && r.decision !== "deny") return null;
|
|
15416
|
+
if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
|
|
15417
|
+
const label2 = boundedString(r.label, 200);
|
|
15418
|
+
if (!label2) return null;
|
|
15419
|
+
const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
|
|
15420
|
+
if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
|
|
15421
|
+
const out = {
|
|
15422
|
+
decision: r.decision,
|
|
15423
|
+
decidedBy: r.decidedBy,
|
|
15424
|
+
label: label2,
|
|
15425
|
+
decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
|
|
15426
|
+
};
|
|
15427
|
+
if (channel) out.channel = channel;
|
|
15428
|
+
const redirect = boundedString(r.redirect, 2e3);
|
|
15429
|
+
if (redirect) out.redirect = redirect;
|
|
15430
|
+
const reason = boundedString(r.reason, 2e3);
|
|
15431
|
+
if (reason) out.reason = reason;
|
|
15432
|
+
const ruleName = boundedString(r.ruleName, 256);
|
|
15433
|
+
if (ruleName) out.ruleName = ruleName;
|
|
15434
|
+
if (typeof r.standing === "string" && STANDING.has(r.standing))
|
|
15435
|
+
out.standing = r.standing;
|
|
15436
|
+
const trustDuration = boundedString(r.trustDuration, 8);
|
|
15437
|
+
if (trustDuration) out.trustDuration = trustDuration;
|
|
15438
|
+
return out;
|
|
15439
|
+
}
|
|
15440
|
+
function legacyWaitFields(record) {
|
|
15441
|
+
switch (record.decidedBy) {
|
|
15442
|
+
case "person": {
|
|
15443
|
+
const source = record.redirect ? "terminal-redirect" : record.channel === "native" ? "native" : "terminal";
|
|
15444
|
+
const reason = record.redirect ?? record.reason;
|
|
15445
|
+
return { decision: record.decision, source, ...reason && { reason } };
|
|
15446
|
+
}
|
|
15447
|
+
case "timer":
|
|
15448
|
+
return {
|
|
15449
|
+
decision: record.decision,
|
|
15450
|
+
source: "timeout",
|
|
15451
|
+
...record.reason && { reason: record.reason }
|
|
15452
|
+
};
|
|
15453
|
+
case "cloud":
|
|
15454
|
+
case "policy":
|
|
15455
|
+
return {
|
|
15456
|
+
decision: record.decision,
|
|
15457
|
+
source: record.decidedBy,
|
|
15458
|
+
...record.reason && { reason: record.reason }
|
|
15459
|
+
};
|
|
15460
|
+
case "abandoned":
|
|
15461
|
+
return { decision: "abandoned" };
|
|
15462
|
+
}
|
|
15463
|
+
}
|
|
15464
|
+
function recordFromLegacyWait(body, now = Date.now()) {
|
|
15465
|
+
if (body.decision === "abandoned") return abandonedRecord(now);
|
|
15466
|
+
const decision = body.decision;
|
|
15467
|
+
switch (body.source) {
|
|
15468
|
+
case "terminal":
|
|
15469
|
+
case "browser":
|
|
15470
|
+
return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
|
|
15471
|
+
case "native":
|
|
15472
|
+
return personRecord("native", decision, {}, now);
|
|
15473
|
+
case "terminal-redirect":
|
|
15474
|
+
return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
|
|
15475
|
+
case "timeout":
|
|
15476
|
+
return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
|
|
15477
|
+
case "cloud":
|
|
15478
|
+
return {
|
|
15479
|
+
decision,
|
|
15480
|
+
decidedBy: "cloud",
|
|
15481
|
+
label: CLOUD_LABEL,
|
|
15482
|
+
...body.reason && { reason: body.reason },
|
|
15483
|
+
decidedAt: now
|
|
15484
|
+
};
|
|
15485
|
+
case "policy":
|
|
15486
|
+
return {
|
|
15487
|
+
decision,
|
|
15488
|
+
decidedBy: "policy",
|
|
15489
|
+
label: "Policy",
|
|
15490
|
+
...body.reason && { reason: body.reason },
|
|
15491
|
+
decidedAt: now
|
|
15492
|
+
};
|
|
15493
|
+
default:
|
|
15494
|
+
return null;
|
|
15495
|
+
}
|
|
15496
|
+
}
|
|
15497
|
+
|
|
15498
|
+
// src/auth/daemon.ts
|
|
15346
15499
|
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
|
|
15347
15500
|
function notifyActivitySocket(data) {
|
|
15348
15501
|
return new Promise((resolve) => {
|
|
@@ -15440,7 +15593,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
15440
15593
|
return false;
|
|
15441
15594
|
}
|
|
15442
15595
|
}
|
|
15443
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
15596
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
|
|
15444
15597
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15445
15598
|
const ctrl = new AbortController();
|
|
15446
15599
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -15466,19 +15619,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
15466
15619
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
15467
15620
|
...viewOnly && { viewOnly: true },
|
|
15468
15621
|
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
15469
|
-
...sessionId && { sessionId }
|
|
15622
|
+
...sessionId && { sessionId },
|
|
15623
|
+
...cardFields(card)
|
|
15470
15624
|
}),
|
|
15471
15625
|
signal: ctrl.signal
|
|
15472
15626
|
});
|
|
15473
15627
|
if (!res.ok) throw new Error("Daemon fail");
|
|
15474
|
-
|
|
15475
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
15628
|
+
return registeredCard(await res.json());
|
|
15476
15629
|
} finally {
|
|
15477
15630
|
clearTimeout(timer);
|
|
15478
15631
|
}
|
|
15479
15632
|
}
|
|
15633
|
+
function cardFields(card) {
|
|
15634
|
+
return {
|
|
15635
|
+
// A review card: the hook already decided this call needs a person; the
|
|
15636
|
+
// daemon never checks it again (approval-card design, defect 1).
|
|
15637
|
+
kind: "review",
|
|
15638
|
+
// Protocol 2 makes the card's /wait require the internal token: claim it
|
|
15639
|
+
// only when this process can read the token (a daemon under another uid
|
|
15640
|
+
// is running but its pid file probe fails with EPERM), or every wait
|
|
15641
|
+
// would get 403 and the review would be denied unseen.
|
|
15642
|
+
...getInternalToken() && { protocol: CARD_PROTOCOL },
|
|
15643
|
+
...card?.channels && { channels: card.channels },
|
|
15644
|
+
...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
|
|
15645
|
+
};
|
|
15646
|
+
}
|
|
15647
|
+
function registeredCard(body) {
|
|
15648
|
+
const b = body;
|
|
15649
|
+
return {
|
|
15650
|
+
id: b.id,
|
|
15651
|
+
allowCount: b.allowCount ?? 1,
|
|
15652
|
+
...b.protocol === CARD_PROTOCOL && {
|
|
15653
|
+
protocol: CARD_PROTOCOL,
|
|
15654
|
+
deadline: typeof b.deadline === "number" ? b.deadline : null
|
|
15655
|
+
}
|
|
15656
|
+
};
|
|
15657
|
+
}
|
|
15480
15658
|
async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
15481
|
-
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15482
15659
|
const waitCtrl = new AbortController();
|
|
15483
15660
|
let capped = false;
|
|
15484
15661
|
const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
|
|
@@ -15488,14 +15665,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15488
15665
|
const onAbort = () => waitCtrl.abort();
|
|
15489
15666
|
if (signal) signal.addEventListener("abort", onAbort);
|
|
15490
15667
|
try {
|
|
15491
|
-
const
|
|
15668
|
+
const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
|
|
15492
15669
|
(err) => {
|
|
15493
15670
|
if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
|
|
15494
15671
|
throw err;
|
|
15495
15672
|
}
|
|
15496
15673
|
);
|
|
15497
|
-
if (
|
|
15498
|
-
|
|
15674
|
+
if (status === 409) throw new Error("node9 card was withdrawn");
|
|
15675
|
+
if (status !== 200) return { decision: "deny" };
|
|
15676
|
+
const parsed = JSON.parse(body);
|
|
15677
|
+
const record = parseDecisionRecord(parsed.record);
|
|
15678
|
+
if (record) return { ...legacyWaitFields(record), record };
|
|
15679
|
+
const { decision, source, reason } = parsed;
|
|
15499
15680
|
if (decision === "allow") return { decision: "allow", source };
|
|
15500
15681
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
15501
15682
|
return { decision: "deny", source, reason };
|
|
@@ -15504,7 +15685,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15504
15685
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
15505
15686
|
}
|
|
15506
15687
|
}
|
|
15507
|
-
|
|
15688
|
+
function daemonGet(urlPath, signal, token = getInternalToken()) {
|
|
15689
|
+
return new Promise((resolve, reject) => {
|
|
15690
|
+
const req = import_http.default.get(
|
|
15691
|
+
{
|
|
15692
|
+
host: DAEMON_HOST,
|
|
15693
|
+
port: DAEMON_PORT,
|
|
15694
|
+
path: urlPath,
|
|
15695
|
+
signal,
|
|
15696
|
+
agent: false,
|
|
15697
|
+
headers: token ? { "X-Node9-Internal": token } : {}
|
|
15698
|
+
},
|
|
15699
|
+
(res) => {
|
|
15700
|
+
let body = "";
|
|
15701
|
+
res.setEncoding("utf8");
|
|
15702
|
+
res.on("data", (chunk) => body += chunk);
|
|
15703
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
15704
|
+
res.on("error", reject);
|
|
15705
|
+
}
|
|
15706
|
+
);
|
|
15707
|
+
req.on("error", reject);
|
|
15708
|
+
});
|
|
15709
|
+
}
|
|
15710
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
|
|
15508
15711
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15509
15712
|
const res = await fetch(`${base2}/check`, {
|
|
15510
15713
|
method: "POST",
|
|
@@ -15520,13 +15723,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
|
|
|
15520
15723
|
// event via socket. Same contract as registerDaemonEntry — without
|
|
15521
15724
|
// it the daemon double-emits 'activity' for cloud-enforced flows.
|
|
15522
15725
|
fromCLI: socketActivitySent !== false,
|
|
15523
|
-
activityId
|
|
15726
|
+
activityId,
|
|
15727
|
+
...extra?.cwd && { cwd: extra.cwd },
|
|
15728
|
+
...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
|
|
15729
|
+
...extra?.sessionId && { sessionId: extra.sessionId },
|
|
15730
|
+
...cardFields(extra?.card)
|
|
15524
15731
|
}),
|
|
15525
15732
|
signal: AbortSignal.timeout(3e3)
|
|
15526
15733
|
});
|
|
15527
15734
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
15528
|
-
|
|
15529
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
15735
|
+
return registeredCard(await res.json());
|
|
15530
15736
|
}
|
|
15531
15737
|
async function notifyTaint(filePath, source, fromEid) {
|
|
15532
15738
|
if (!isDaemonRunning()) return;
|
|
@@ -15619,14 +15825,18 @@ async function approveSessionExposure(sessionId, keys) {
|
|
|
15619
15825
|
} catch {
|
|
15620
15826
|
}
|
|
15621
15827
|
}
|
|
15622
|
-
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
15828
|
+
async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
15623
15829
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15624
|
-
await fetch(`${base2}/resolve/${id}`, {
|
|
15830
|
+
const res = await fetch(`${base2}/resolve/${id}`, {
|
|
15625
15831
|
method: "POST",
|
|
15626
15832
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
15627
|
-
body: JSON.stringify({ decision, ...source && { source } }),
|
|
15833
|
+
body: JSON.stringify({ decision, ...source && { source }, ...record && { record } }),
|
|
15628
15834
|
signal: AbortSignal.timeout(3e3)
|
|
15629
15835
|
});
|
|
15836
|
+
if (!res.ok) return null;
|
|
15837
|
+
const body = await res.json().catch(() => null);
|
|
15838
|
+
const winner = parseDecisionRecord(body?.record);
|
|
15839
|
+
return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
|
|
15630
15840
|
}
|
|
15631
15841
|
|
|
15632
15842
|
// src/auth/orchestrator.ts
|
|
@@ -17220,17 +17430,98 @@ function asApprovalRequest(description) {
|
|
|
17220
17430
|
return description;
|
|
17221
17431
|
}
|
|
17222
17432
|
|
|
17223
|
-
// src/auth/
|
|
17224
|
-
var
|
|
17225
|
-
|
|
17226
|
-
"
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
...
|
|
17232
|
-
|
|
17233
|
-
|
|
17433
|
+
// src/auth/card-result.ts
|
|
17434
|
+
var CHANNEL_NAMES = {
|
|
17435
|
+
terminal: "Terminal (node9 tail)",
|
|
17436
|
+
monitor: "node9 monitor",
|
|
17437
|
+
native: "system dialog window",
|
|
17438
|
+
cloud: "organization"
|
|
17439
|
+
};
|
|
17440
|
+
function authResultFromRecord(r, ctx = {}) {
|
|
17441
|
+
const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
|
|
17442
|
+
switch (r.decidedBy) {
|
|
17443
|
+
case "person": {
|
|
17444
|
+
const channel = r.channel ?? "terminal";
|
|
17445
|
+
const decisionSource = channel === "native" ? "native" : "terminal";
|
|
17446
|
+
if (r.decision === "allow") {
|
|
17447
|
+
return { approved: true, checkedBy: "daemon", decisionSource, ...who };
|
|
17448
|
+
}
|
|
17449
|
+
return {
|
|
17450
|
+
approved: false,
|
|
17451
|
+
reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
|
|
17452
|
+
blockedBy: "local-decision",
|
|
17453
|
+
blockedByLabel: r.label,
|
|
17454
|
+
decisionSource,
|
|
17455
|
+
...who,
|
|
17456
|
+
...r.redirect && { redirect: r.redirect }
|
|
17457
|
+
};
|
|
17458
|
+
}
|
|
17459
|
+
case "timer":
|
|
17460
|
+
if (r.decision === "deny" && ctx.downgradedBlock)
|
|
17461
|
+
return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
|
|
17462
|
+
return r.decision === "allow" ? { approved: true, ...who } : {
|
|
17463
|
+
approved: false,
|
|
17464
|
+
reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
|
|
17465
|
+
blockedBy: "timeout",
|
|
17466
|
+
blockedByLabel: TIMER_LABEL,
|
|
17467
|
+
...who
|
|
17468
|
+
};
|
|
17469
|
+
case "cloud":
|
|
17470
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
|
|
17471
|
+
approved: false,
|
|
17472
|
+
reason: r.reason || "Action rejected by organization administrator via Slack.",
|
|
17473
|
+
blockedBy: "team-policy",
|
|
17474
|
+
blockedByLabel: CLOUD_LABEL,
|
|
17475
|
+
decisionSource: "cloud",
|
|
17476
|
+
...who
|
|
17477
|
+
};
|
|
17478
|
+
case "policy":
|
|
17479
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
|
|
17480
|
+
approved: false,
|
|
17481
|
+
reason: r.reason || "Action blocked by policy.",
|
|
17482
|
+
blockedBy: "local-config",
|
|
17483
|
+
blockedByLabel: r.label,
|
|
17484
|
+
...r.ruleName && { ruleHit: r.ruleName },
|
|
17485
|
+
...who
|
|
17486
|
+
};
|
|
17487
|
+
case "abandoned":
|
|
17488
|
+
return {
|
|
17489
|
+
approved: false,
|
|
17490
|
+
reason: r.reason || "No approver was left to answer.",
|
|
17491
|
+
blockedBy: "no-approval-mechanism",
|
|
17492
|
+
blockedByLabel: ABANDONED_LABEL,
|
|
17493
|
+
...who
|
|
17494
|
+
};
|
|
17495
|
+
}
|
|
17496
|
+
}
|
|
17497
|
+
function approvalUnavailable(reason) {
|
|
17498
|
+
return {
|
|
17499
|
+
approved: false,
|
|
17500
|
+
reason: reason || "node9 could not get an approval decision for this call.",
|
|
17501
|
+
blockedBy: "no-approval-mechanism",
|
|
17502
|
+
blockedByLabel: "Approval Unavailable"
|
|
17503
|
+
};
|
|
17504
|
+
}
|
|
17505
|
+
function decidedByOf(res) {
|
|
17506
|
+
if (res.decidedBy) return res.decidedBy;
|
|
17507
|
+
if (res.blockedBy === "timeout") return "timer";
|
|
17508
|
+
if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
|
|
17509
|
+
return "policy";
|
|
17510
|
+
}
|
|
17511
|
+
function recordFromAuthResult(res, now = Date.now()) {
|
|
17512
|
+
const decidedBy = decidedByOf(res);
|
|
17513
|
+
const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
|
|
17514
|
+
return {
|
|
17515
|
+
decision: res.approved ? "allow" : "deny",
|
|
17516
|
+
decidedBy,
|
|
17517
|
+
...channel && { channel },
|
|
17518
|
+
label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
|
|
17519
|
+
...res.reason && { reason: res.reason.slice(0, 2e3) },
|
|
17520
|
+
...res.ruleHit && { ruleName: res.ruleHit },
|
|
17521
|
+
...res.redirect && { redirect: res.redirect },
|
|
17522
|
+
decidedAt: now
|
|
17523
|
+
};
|
|
17524
|
+
}
|
|
17234
17525
|
|
|
17235
17526
|
// src/auth/orchestrator.ts
|
|
17236
17527
|
var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
|
|
@@ -18270,28 +18561,22 @@ ${packageReview}`;
|
|
|
18270
18561
|
const { signal } = abortController;
|
|
18271
18562
|
const racePromises = [];
|
|
18272
18563
|
const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
|
|
18273
|
-
if (approvalTimeoutMs > 0) {
|
|
18274
|
-
racePromises.push(
|
|
18275
|
-
new Promise((resolve, reject) => {
|
|
18276
|
-
const timer = setTimeout(() => {
|
|
18277
|
-
resolve({
|
|
18278
|
-
approved: false,
|
|
18279
|
-
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
18280
|
-
blockedBy: "timeout",
|
|
18281
|
-
blockedByLabel: "Approval Timeout"
|
|
18282
|
-
});
|
|
18283
|
-
}, approvalTimeoutMs);
|
|
18284
|
-
signal.addEventListener("abort", () => {
|
|
18285
|
-
clearTimeout(timer);
|
|
18286
|
-
reject(new Error("Aborted"));
|
|
18287
|
-
});
|
|
18288
|
-
})
|
|
18289
|
-
);
|
|
18290
|
-
}
|
|
18291
18564
|
let viewerId = null;
|
|
18292
18565
|
const internalToken = getInternalToken();
|
|
18293
18566
|
let daemonEntryId = null;
|
|
18294
18567
|
let daemonAllowCount = 1;
|
|
18568
|
+
let cardDeadline;
|
|
18569
|
+
const cardOptions = {
|
|
18570
|
+
channels: [
|
|
18571
|
+
"terminal",
|
|
18572
|
+
"monitor",
|
|
18573
|
+
// Only when the popup can show: a card listing a channel nobody can
|
|
18574
|
+
// use is never abandoned when node9 tail closes, and waits unanswered.
|
|
18575
|
+
...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
|
|
18576
|
+
...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
|
|
18577
|
+
],
|
|
18578
|
+
timeoutMs: approvalTimeoutMs
|
|
18579
|
+
};
|
|
18295
18580
|
if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
18296
18581
|
if (cloudEnforced && cloudRequestId) {
|
|
18297
18582
|
const viewer = await notifyDaemonViewer(
|
|
@@ -18300,11 +18585,20 @@ ${packageReview}`;
|
|
|
18300
18585
|
meta,
|
|
18301
18586
|
riskMetadata,
|
|
18302
18587
|
options?.activityId,
|
|
18303
|
-
options?.socketActivitySent
|
|
18588
|
+
options?.socketActivitySent,
|
|
18589
|
+
{
|
|
18590
|
+
cwd: options?.cwd,
|
|
18591
|
+
recoveryCommand: statefulRecoveryCommand,
|
|
18592
|
+
sessionId: meta?.sessionId,
|
|
18593
|
+
card: cardOptions
|
|
18594
|
+
}
|
|
18304
18595
|
).catch(() => null);
|
|
18305
18596
|
viewerId = viewer?.id ?? null;
|
|
18306
18597
|
daemonEntryId = viewerId;
|
|
18307
|
-
if (viewer)
|
|
18598
|
+
if (viewer) {
|
|
18599
|
+
daemonAllowCount = viewer.allowCount;
|
|
18600
|
+
cardDeadline = viewer.deadline;
|
|
18601
|
+
}
|
|
18308
18602
|
} else {
|
|
18309
18603
|
try {
|
|
18310
18604
|
const entry = await registerDaemonEntry(
|
|
@@ -18328,14 +18622,35 @@ ${packageReview}`;
|
|
|
18328
18622
|
void 0,
|
|
18329
18623
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
18330
18624
|
options?.socketActivitySent,
|
|
18331
|
-
meta?.sessionId
|
|
18625
|
+
meta?.sessionId,
|
|
18626
|
+
cardOptions
|
|
18332
18627
|
);
|
|
18333
18628
|
daemonEntryId = entry.id;
|
|
18334
18629
|
daemonAllowCount = entry.allowCount;
|
|
18630
|
+
cardDeadline = entry.deadline;
|
|
18335
18631
|
} catch {
|
|
18336
18632
|
}
|
|
18337
18633
|
}
|
|
18338
18634
|
}
|
|
18635
|
+
if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
|
|
18636
|
+
racePromises.push(
|
|
18637
|
+
new Promise((resolve, reject) => {
|
|
18638
|
+
const timer = setTimeout(() => {
|
|
18639
|
+
resolve({
|
|
18640
|
+
approved: false,
|
|
18641
|
+
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
18642
|
+
blockedBy: "timeout",
|
|
18643
|
+
blockedByLabel: "Approval Timeout",
|
|
18644
|
+
decidedBy: "timer"
|
|
18645
|
+
});
|
|
18646
|
+
}, approvalTimeoutMs);
|
|
18647
|
+
signal.addEventListener("abort", () => {
|
|
18648
|
+
clearTimeout(timer);
|
|
18649
|
+
reject(new Error("Aborted"));
|
|
18650
|
+
});
|
|
18651
|
+
})
|
|
18652
|
+
);
|
|
18653
|
+
}
|
|
18339
18654
|
if (cloudEnforced && cloudRequestId && cloudMayVote) {
|
|
18340
18655
|
racePromises.push(
|
|
18341
18656
|
(async () => {
|
|
@@ -18346,7 +18661,9 @@ ${packageReview}`;
|
|
|
18346
18661
|
reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
|
|
18347
18662
|
checkedBy: cloudResult.approved ? "cloud" : void 0,
|
|
18348
18663
|
blockedBy: cloudResult.approved ? void 0 : "team-policy",
|
|
18349
|
-
blockedByLabel: "Organization Policy (SaaS)"
|
|
18664
|
+
blockedByLabel: "Organization Policy (SaaS)",
|
|
18665
|
+
decidedBy: "cloud",
|
|
18666
|
+
channel: "cloud"
|
|
18350
18667
|
};
|
|
18351
18668
|
} catch (err) {
|
|
18352
18669
|
const error = err;
|
|
@@ -18376,14 +18693,31 @@ ${packageReview}`;
|
|
|
18376
18693
|
if (config.policySource !== "workspace") {
|
|
18377
18694
|
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
18378
18695
|
}
|
|
18379
|
-
return {
|
|
18696
|
+
return {
|
|
18697
|
+
approved: true,
|
|
18698
|
+
checkedBy: "daemon",
|
|
18699
|
+
decisionSource: "native",
|
|
18700
|
+
decidedBy: "person",
|
|
18701
|
+
channel: "native"
|
|
18702
|
+
};
|
|
18380
18703
|
}
|
|
18381
18704
|
if (decision === "always_allow") {
|
|
18382
18705
|
if (standingTrustApplies()) {
|
|
18383
18706
|
writeTrustSession(toolName, 36e5, args);
|
|
18384
|
-
return {
|
|
18707
|
+
return {
|
|
18708
|
+
approved: true,
|
|
18709
|
+
checkedBy: "trust",
|
|
18710
|
+
decidedBy: "person",
|
|
18711
|
+
channel: "native"
|
|
18712
|
+
};
|
|
18385
18713
|
}
|
|
18386
|
-
return {
|
|
18714
|
+
return {
|
|
18715
|
+
approved: true,
|
|
18716
|
+
checkedBy: "daemon",
|
|
18717
|
+
decisionSource: "native",
|
|
18718
|
+
decidedBy: "person",
|
|
18719
|
+
channel: "native"
|
|
18720
|
+
};
|
|
18387
18721
|
}
|
|
18388
18722
|
const isApproved = decision === "allow";
|
|
18389
18723
|
return {
|
|
@@ -18392,75 +18726,32 @@ ${packageReview}`;
|
|
|
18392
18726
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
18393
18727
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18394
18728
|
blockedByLabel: "User Decision (Native)",
|
|
18395
|
-
decisionSource: "native"
|
|
18729
|
+
decisionSource: "native",
|
|
18730
|
+
decidedBy: "person",
|
|
18731
|
+
channel: "native"
|
|
18396
18732
|
};
|
|
18397
18733
|
})()
|
|
18398
18734
|
);
|
|
18399
18735
|
}
|
|
18736
|
+
const fromCard = /* @__PURE__ */ new WeakSet();
|
|
18737
|
+
const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
|
|
18400
18738
|
if (daemonEntryId && approvers.terminal) {
|
|
18401
18739
|
racePromises.push(
|
|
18402
18740
|
(async () => {
|
|
18403
|
-
const
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
|
|
18411
|
-
);
|
|
18412
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
18413
|
-
const isApproved = daemonDecision === "allow";
|
|
18414
|
-
const isRedirect = decisionSource === REDIRECT_SOURCE;
|
|
18415
|
-
if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
|
|
18416
|
-
if (decisionSource === "cloud")
|
|
18417
|
-
return {
|
|
18418
|
-
approved: false,
|
|
18419
|
-
reason: daemonReason || "Action rejected by organization administrator via Slack.",
|
|
18420
|
-
blockedBy: "team-policy",
|
|
18421
|
-
blockedByLabel: "Organization Policy (SaaS)",
|
|
18422
|
-
decisionSource: "cloud"
|
|
18423
|
-
};
|
|
18424
|
-
const timedOut = decisionSource === "timeout";
|
|
18425
|
-
if (downgradedBlock)
|
|
18426
|
-
return {
|
|
18427
|
-
...downgradedBlock,
|
|
18428
|
-
blockedBy: timedOut ? "timeout" : "local-config"
|
|
18429
|
-
};
|
|
18430
|
-
if (timedOut)
|
|
18431
|
-
return {
|
|
18432
|
-
approved: false,
|
|
18433
|
-
reason: daemonReason || "No response: auto-denied after the approval timeout.",
|
|
18434
|
-
blockedBy: "timeout",
|
|
18435
|
-
blockedByLabel: "Approval Timeout"
|
|
18436
|
-
};
|
|
18437
|
-
if (decisionSource === "policy")
|
|
18438
|
-
return {
|
|
18439
|
-
approved: false,
|
|
18440
|
-
reason: daemonReason || "Action blocked by policy.",
|
|
18441
|
-
blockedBy: "local-config",
|
|
18442
|
-
blockedByLabel: explainableLabel
|
|
18443
|
-
};
|
|
18444
|
-
return {
|
|
18445
|
-
approved: false,
|
|
18446
|
-
reason: daemonReason || "node9 could not get an approval decision for this call.",
|
|
18447
|
-
blockedBy: "no-approval-mechanism",
|
|
18448
|
-
blockedByLabel: "Approval Unavailable"
|
|
18449
|
-
};
|
|
18741
|
+
const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
|
|
18742
|
+
const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
|
|
18743
|
+
const legacy = !outcome.record;
|
|
18744
|
+
const record = outcome.record ?? recordFromLegacyWait(outcome);
|
|
18745
|
+
if (downgradedBlock && (!record || legacy && record.decidedBy === "policy" && record.decision === "deny")) {
|
|
18746
|
+
const softened = { ...downgradedBlock, blockedBy: "local-config" };
|
|
18747
|
+
fromCard.add(softened);
|
|
18748
|
+
return softened;
|
|
18450
18749
|
}
|
|
18451
|
-
if (
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
return {
|
|
18457
|
-
approved: isApproved,
|
|
18458
|
-
reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
|
|
18459
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
18460
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18461
|
-
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
18462
|
-
decisionSource: "terminal"
|
|
18463
|
-
};
|
|
18750
|
+
if (!record) return approvalUnavailable(outcome.reason);
|
|
18751
|
+
if (legacy && record.decidedBy === "policy") record.label = explainableLabel;
|
|
18752
|
+
const result = authResultFromRecord(record, { downgradedBlock, timeoutReason });
|
|
18753
|
+
fromCard.add(result);
|
|
18754
|
+
return result;
|
|
18464
18755
|
})()
|
|
18465
18756
|
);
|
|
18466
18757
|
}
|
|
@@ -18492,7 +18783,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18492
18783
|
blockedByLabel: explainableLabel
|
|
18493
18784
|
};
|
|
18494
18785
|
}
|
|
18495
|
-
const
|
|
18786
|
+
const raceResult = await new Promise((resolve) => {
|
|
18496
18787
|
let resolved = false;
|
|
18497
18788
|
let failures = 0;
|
|
18498
18789
|
const total = racePromises.length;
|
|
@@ -18500,14 +18791,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18500
18791
|
if (!resolved) {
|
|
18501
18792
|
resolved = true;
|
|
18502
18793
|
abortController.abort();
|
|
18503
|
-
if (daemonEntryId && internalToken) {
|
|
18504
|
-
resolveViaDaemon(
|
|
18505
|
-
daemonEntryId,
|
|
18506
|
-
res.approved ? "allow" : "deny",
|
|
18507
|
-
internalToken,
|
|
18508
|
-
res.decisionSource
|
|
18509
|
-
).catch(() => null);
|
|
18510
|
-
}
|
|
18511
18794
|
resolve(res);
|
|
18512
18795
|
}
|
|
18513
18796
|
};
|
|
@@ -18515,26 +18798,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18515
18798
|
p.then(finish).catch((err) => {
|
|
18516
18799
|
if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
|
|
18517
18800
|
return;
|
|
18518
|
-
if (err.message === "Abandoned") {
|
|
18519
|
-
finish({
|
|
18520
|
-
approved: false,
|
|
18521
|
-
reason: "Browser dashboard closed without making a decision.",
|
|
18522
|
-
blockedBy: "local-decision",
|
|
18523
|
-
blockedByLabel: "Browser Dashboard (Abandoned)"
|
|
18524
|
-
});
|
|
18525
|
-
return;
|
|
18526
|
-
}
|
|
18527
18801
|
failures++;
|
|
18528
18802
|
if (failures === total && !resolved) {
|
|
18529
|
-
finish(
|
|
18803
|
+
finish(approvalUnavailable("All approval channels failed or disconnected."));
|
|
18530
18804
|
}
|
|
18531
18805
|
});
|
|
18532
18806
|
}
|
|
18533
18807
|
});
|
|
18808
|
+
let finalResult = raceResult;
|
|
18809
|
+
if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
|
|
18810
|
+
const reply = await resolveViaDaemon(
|
|
18811
|
+
daemonEntryId,
|
|
18812
|
+
raceResult.approved ? "allow" : "deny",
|
|
18813
|
+
internalToken,
|
|
18814
|
+
raceResult.decisionSource,
|
|
18815
|
+
recordFromAuthResult(raceResult)
|
|
18816
|
+
).catch(() => null);
|
|
18817
|
+
if (reply && !reply.won)
|
|
18818
|
+
finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
|
|
18819
|
+
}
|
|
18534
18820
|
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
18535
18821
|
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
18536
18822
|
}
|
|
18537
|
-
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
18823
|
+
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
|
|
18538
18824
|
await resolveNode9SaaS(
|
|
18539
18825
|
cloudRequestId,
|
|
18540
18826
|
creds,
|
|
@@ -18558,7 +18844,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18558
18844
|
{
|
|
18559
18845
|
...ownRowMeta(finalResult.approved),
|
|
18560
18846
|
...cloudRequestId && { cloudRequestId },
|
|
18561
|
-
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
|
|
18847
|
+
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
|
|
18848
|
+
// Who decided, from the decision record, and the card: the daemon
|
|
18849
|
+
// writes its own row for the same card, and the report counts one.
|
|
18850
|
+
...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
|
|
18851
|
+
...finalResult.channel && { channel: finalResult.channel },
|
|
18852
|
+
...daemonEntryId && { cardId: daemonEntryId }
|
|
18562
18853
|
},
|
|
18563
18854
|
hashAuditArgs
|
|
18564
18855
|
);
|