@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/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
- const { id, allowCount } = await res.json();
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 waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
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 (!waitRes.ok) return { decision: "deny" };
15498
- const { decision, source, reason } = await waitRes.json();
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
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
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
- const { id, allowCount } = await res.json();
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/decision-sources.ts
17224
- var HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
17225
- "terminal",
17226
- "browser",
17227
- "native"
17228
- ]);
17229
- var REDIRECT_SOURCE = "terminal-redirect";
17230
- var DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
17231
- ...HUMAN_DECISION_SOURCES,
17232
- REDIRECT_SOURCE
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) daemonAllowCount = viewer.allowCount;
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 { approved: true, checkedBy: "daemon", decisionSource: "native" };
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 { approved: true, checkedBy: "trust" };
18707
+ return {
18708
+ approved: true,
18709
+ checkedBy: "trust",
18710
+ decidedBy: "person",
18711
+ channel: "native"
18712
+ };
18385
18713
  }
18386
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
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
- decision: daemonDecision,
18405
- source: decisionSource,
18406
- reason: daemonReason
18407
- } = await waitForDaemonDecision(
18408
- daemonEntryId,
18409
- signal,
18410
- daemonWaitCapMs(config.settings.approvalTimeoutMs)
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 (isApproved && decisionSource === "cloud")
18452
- return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
18453
- if (isApproved && decisionSource === "policy")
18454
- return { approved: true, checkedBy: "local-policy" };
18455
- const via = "Terminal (node9 tail)";
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 finalResult = await new Promise((resolve) => {
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({ approved: false, reason: "All approval channels failed or disconnected." });
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
  );