@node9/proxy 1.29.0 → 1.30.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.
@@ -2220,7 +2220,13 @@ var init_config_schema = __esm({
2220
2220
  allowGlobalPause: z.boolean().optional(),
2221
2221
  auditHashArgs: z.boolean().optional(),
2222
2222
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
2223
- cloudSyncIntervalHours: z.number().positive().optional()
2223
+ cloudSyncIntervalHours: z.number().positive().optional(),
2224
+ // Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
2225
+ // to true; set false to fall back to local-only auditing.
2226
+ shipper: z.object({
2227
+ enabled: z.boolean().optional(),
2228
+ intervalSeconds: z.number().min(5).optional()
2229
+ }).optional()
2224
2230
  }).optional(),
2225
2231
  policy: z.object({
2226
2232
  sandboxPaths: z.array(z.string()).optional(),
@@ -2355,7 +2361,8 @@ var init_config = __esm({
2355
2361
  flightRecorder: true,
2356
2362
  auditHashArgs: true,
2357
2363
  approvers: { native: true, browser: false, cloud: false, terminal: true },
2358
- cloudSyncIntervalHours: 5
2364
+ cloudSyncIntervalHours: 5,
2365
+ shipper: { enabled: true, intervalSeconds: 20 }
2359
2366
  },
2360
2367
  policy: {
2361
2368
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
package/dist/index.js CHANGED
@@ -74,8 +74,13 @@ __export(audit_exports, {
74
74
  appendHookDebug: () => appendHookDebug,
75
75
  appendLocalAudit: () => appendLocalAudit,
76
76
  appendToLog: () => appendToLog,
77
+ buildArgsPreview: () => buildArgsPreview,
78
+ generateEventId: () => generateEventId,
77
79
  redactSecrets: () => redactSecrets
78
80
  });
81
+ function generateEventId() {
82
+ return `${Date.now().toString(36)}-${import_crypto2.default.randomBytes(6).toString("hex")}`;
83
+ }
79
84
  function isTestCall(toolName, args) {
80
85
  if (toolName !== "Bash" && toolName !== "bash") return false;
81
86
  const cmd = args?.command;
@@ -94,6 +99,17 @@ function redactSecrets(text) {
94
99
  );
95
100
  return redacted;
96
101
  }
102
+ function buildArgsPreview(args) {
103
+ try {
104
+ const o = args && typeof args === "object" ? args : null;
105
+ const primary = o && (o.command ?? o.file_path ?? o.path ?? o.url ?? o.query);
106
+ const text = typeof primary === "string" ? primary : args ? JSON.stringify(args) : "";
107
+ if (!text) return void 0;
108
+ return redactSecrets(text).slice(0, 120);
109
+ } catch {
110
+ return void 0;
111
+ }
112
+ }
97
113
  function appendToLog(logPath, entry) {
98
114
  try {
99
115
  const dir = import_path.default.dirname(logPath);
@@ -116,11 +132,18 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
116
132
  });
117
133
  }
118
134
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
119
- const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
135
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
136
+ const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
137
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
138
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
139
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
122
140
  const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
141
+ const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
142
+ const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
123
143
  appendToLog(LOCAL_AUDIT_LOG, {
144
+ // eid first: the outbox shipper dedups on it, and a fixed leading field
145
+ // makes the JSONL easy to eyeball.
146
+ eid: generateEventId(),
124
147
  ts: (/* @__PURE__ */ new Date()).toISOString(),
125
148
  tool: toolName,
126
149
  ...agentToolNameField,
@@ -128,6 +151,8 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
128
151
  decision,
129
152
  checkedBy,
130
153
  ...ruleNameField,
154
+ ...dlpFields,
155
+ ...cloudLinkField,
131
156
  ...testRun,
132
157
  agent: meta?.agent,
133
158
  mcpServer: meta?.mcpServer,
@@ -142,13 +167,14 @@ function appendConfigAudit(entry) {
142
167
  hostname: import_os.default.hostname()
143
168
  });
144
169
  }
145
- var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
170
+ var import_fs, import_path, import_os, import_crypto2, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
146
171
  var init_audit = __esm({
147
172
  "src/audit/index.ts"() {
148
173
  "use strict";
149
174
  import_fs = __toESM(require("fs"));
150
175
  import_path = __toESM(require("path"));
151
176
  import_os = __toESM(require("os"));
177
+ import_crypto2 = __toESM(require("crypto"));
152
178
  init_hasher();
153
179
  LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
154
180
  HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
@@ -246,7 +272,13 @@ var ConfigFileSchema = import_zod.z.object({
246
272
  allowGlobalPause: import_zod.z.boolean().optional(),
247
273
  auditHashArgs: import_zod.z.boolean().optional(),
248
274
  agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
249
- cloudSyncIntervalHours: import_zod.z.number().positive().optional()
275
+ cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
276
+ // Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
277
+ // to true; set false to fall back to local-only auditing.
278
+ shipper: import_zod.z.object({
279
+ enabled: import_zod.z.boolean().optional(),
280
+ intervalSeconds: import_zod.z.number().min(5).optional()
281
+ }).optional()
250
282
  }).optional(),
251
283
  policy: import_zod.z.object({
252
284
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
@@ -314,7 +346,7 @@ var import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
314
346
  var import_picomatch = __toESM(require("picomatch"), 1);
315
347
  var import_safe_regex22 = __toESM(require("safe-regex2"), 1);
316
348
  var import_safe_regex23 = __toESM(require("safe-regex2"), 1);
317
- var import_crypto2 = __toESM(require("crypto"), 1);
349
+ var import_crypto3 = __toESM(require("crypto"), 1);
318
350
  var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
319
351
  function isAssignmentContext(text) {
320
352
  return ASSIGNMENT_CONTEXT_RE.test(text);
@@ -2847,7 +2879,7 @@ assertBuiltinShieldRegexesAreSafe();
2847
2879
  var LOOP_MAX_RECORDS = 500;
2848
2880
  function computeArgsHash(args) {
2849
2881
  const str = JSON.stringify(args ?? "");
2850
- return import_crypto2.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
2882
+ return import_crypto3.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
2851
2883
  }
2852
2884
  function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2853
2885
  const hash = computeArgsHash(args);
@@ -2960,7 +2992,8 @@ var DEFAULT_CONFIG = {
2960
2992
  flightRecorder: true,
2961
2993
  auditHashArgs: true,
2962
2994
  approvers: { native: true, browser: false, cloud: false, terminal: true },
2963
- cloudSyncIntervalHours: 5
2995
+ cloudSyncIntervalHours: 5,
2996
+ shipper: { enabled: true, intervalSeconds: 20 }
2964
2997
  },
2965
2998
  policy: {
2966
2999
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -3255,7 +3288,8 @@ function getConfig(cwd) {
3255
3288
  const projectConfig = tryLoadConfig(projectPath);
3256
3289
  const mergedSettings = {
3257
3290
  ...DEFAULT_CONFIG.settings,
3258
- approvers: { ...DEFAULT_CONFIG.settings.approvers }
3291
+ approvers: { ...DEFAULT_CONFIG.settings.approvers },
3292
+ shipper: { ...DEFAULT_CONFIG.settings.shipper }
3259
3293
  };
3260
3294
  const mergedPolicy = {
3261
3295
  sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
@@ -3286,6 +3320,7 @@ function getConfig(cwd) {
3286
3320
  if (s.enableHookLogDebug !== void 0)
3287
3321
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
3288
3322
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
3323
+ if (s.shipper) mergedSettings.shipper = { ...mergedSettings.shipper, ...s.shipper };
3289
3324
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
3290
3325
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
3291
3326
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
@@ -3948,7 +3983,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
3948
3983
  }
3949
3984
 
3950
3985
  // src/auth/orchestrator.ts
3951
- var import_crypto3 = require("crypto");
3986
+ var import_crypto4 = require("crypto");
3952
3987
 
3953
3988
  // src/ui/native.ts
3954
3989
  var import_child_process = require("child_process");
@@ -4281,91 +4316,6 @@ var import_fs9 = __toESM(require("fs"));
4281
4316
  var import_os8 = __toESM(require("os"));
4282
4317
  var import_path11 = __toESM(require("path"));
4283
4318
  init_audit();
4284
- var DLP_SAMPLE_MAX_LEN = 200;
4285
- var DLP_PATTERN_MAX_LEN = 100;
4286
- var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
4287
- "dlp-block",
4288
- "observe-mode-dlp-would-block",
4289
- "dlp-review-flagged",
4290
- "loop-detected",
4291
- "audit-mode",
4292
- "local-policy",
4293
- "smart-rule-block",
4294
- // Smart-rule block was downgraded to review because the daemon was
4295
- // running and we're not in CI. The block attempt is still recorded;
4296
- // the user got a popup. Distinct from 'smart-rule-block' so the
4297
- // dashboard can show "block rule overridden" separately from a hard
4298
- // block that fired with no human in the loop.
4299
- "smart-rule-block-override",
4300
- "persistent",
4301
- "trust",
4302
- "observe-mode",
4303
- "observe-mode-would-block"
4304
- ]);
4305
- function validateApiUrl(raw) {
4306
- let u;
4307
- try {
4308
- u = new URL(raw);
4309
- } catch {
4310
- return null;
4311
- }
4312
- if (u.username || u.password) return null;
4313
- if (u.protocol === "https:") return u;
4314
- if (u.protocol === "http:") {
4315
- const h = u.hostname;
4316
- if (h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]") return u;
4317
- }
4318
- return null;
4319
- }
4320
- function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
4321
- const validated = validateApiUrl(creds.apiUrl);
4322
- if (!validated) {
4323
- try {
4324
- import_fs9.default.appendFileSync(
4325
- HOOK_DEBUG_LOG,
4326
- `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
4327
- `
4328
- );
4329
- } catch {
4330
- }
4331
- return Promise.resolve();
4332
- }
4333
- const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
4334
- const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
4335
- const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
4336
- const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
4337
- const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
4338
- Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
4339
- ) : void 0;
4340
- const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
4341
- return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
4342
- method: "POST",
4343
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
4344
- body: JSON.stringify({
4345
- toolName,
4346
- args: safeArgs,
4347
- checkedBy: safeCheckedBy,
4348
- ...dlpInfo && { dlpPattern, dlpSample },
4349
- ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
4350
- // session_id (Claude Code + Gemini CLI) groups all audit rows from one
4351
- // agent run; transcript_path is the authoritative pointer to the
4352
- // session log (survives Gemini resume drift). Both optional —
4353
- // unsupported agents (MCP-mediated) leave them undefined.
4354
- ...meta?.sessionId && { runId: meta.sessionId },
4355
- ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
4356
- context: {
4357
- agent: meta?.agent,
4358
- mcpServer: meta?.mcpServer,
4359
- hostname: import_os8.default.hostname(),
4360
- cwd: process.cwd(),
4361
- platform: import_os8.default.platform()
4362
- }
4363
- }),
4364
- signal: AbortSignal.timeout(5e3)
4365
- }).then(() => {
4366
- }).catch(() => {
4367
- });
4368
- }
4369
4319
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
4370
4320
  const controller = new AbortController();
4371
4321
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -4576,7 +4526,7 @@ function notifyActivity(data) {
4576
4526
  }
4577
4527
  async function authorizeHeadless(toolName, args, meta, options) {
4578
4528
  if (!options?.calledFromDaemon) {
4579
- const actId = (0, import_crypto3.randomUUID)();
4529
+ const actId = (0, import_crypto4.randomUUID)();
4580
4530
  const actTs = Date.now();
4581
4531
  const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
4582
4532
  const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
@@ -4671,17 +4621,11 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4671
4621
  args,
4672
4622
  "deny",
4673
4623
  isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
4674
- meta,
4675
- true
4676
- );
4677
- if (approvers.cloud && creds?.apiKey)
4678
- auditLocalAllow(
4679
- toolName,
4680
- args,
4681
- isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
4682
- creds,
4683
- meta,
4684
- { pattern: dlpMatch.patternName, redactedSample: dlpMatch.redactedSample },
4624
+ {
4625
+ ...meta,
4626
+ dlpPattern: dlpMatch.patternName,
4627
+ dlpSample: dlpMatch.redactedSample
4628
+ },
4685
4629
  true
4686
4630
  );
4687
4631
  if (isWriteTool(toolName) && filePath) {
@@ -4737,9 +4681,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4737
4681
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
4738
4682
  if (policyResult.decision === "review") {
4739
4683
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
4740
- if (approvers.cloud && creds?.apiKey) {
4741
- await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
4742
- }
4743
4684
  }
4744
4685
  }
4745
4686
  return { approved: true, checkedBy: "audit" };
@@ -4752,8 +4693,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4752
4693
  const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
4753
4694
  if (!isManual)
4754
4695
  appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
4755
- if (approvers.cloud && creds?.apiKey)
4756
- auditLocalAllow(toolName, args, "loop-detected", creds, meta, void 0, true);
4757
4696
  return {
4758
4697
  approved: false,
4759
4698
  reason,
@@ -4772,15 +4711,15 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4772
4711
  };
4773
4712
  }
4774
4713
  if (policyResult.decision === "allow") {
4775
- if (approvers.cloud && creds?.apiKey)
4776
- await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
4777
- ruleName: policyResult.ruleName,
4778
- ruleDescription: policyResult.ruleDescription,
4779
- blockedByLabel: policyResult.blockedByLabel,
4780
- matchedField: policyResult.matchedField,
4781
- matchedWord: policyResult.matchedWord
4782
- });
4783
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
4714
+ if (!isManual)
4715
+ appendLocalAudit(
4716
+ toolName,
4717
+ args,
4718
+ "allow",
4719
+ "local-policy",
4720
+ { ...meta, ruleName: policyResult.ruleName },
4721
+ hashAuditArgs
4722
+ );
4784
4723
  return { approved: true, checkedBy: "local-policy" };
4785
4724
  }
4786
4725
  if (policyResult.decision === "block") {
@@ -4813,23 +4752,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4813
4752
  { ...meta, ruleName: policyResult.ruleName },
4814
4753
  hashAuditArgs
4815
4754
  );
4816
- if (approvers.cloud && creds?.apiKey)
4817
- auditLocalAllow(
4818
- toolName,
4819
- args,
4820
- "smart-rule-block-override",
4821
- creds,
4822
- meta,
4823
- void 0,
4824
- false,
4825
- {
4826
- ruleName: policyResult.ruleName,
4827
- ruleDescription: policyResult.ruleDescription,
4828
- blockedByLabel: policyResult.blockedByLabel,
4829
- matchedField: policyResult.matchedField,
4830
- matchedWord: policyResult.matchedWord
4831
- }
4832
- );
4833
4755
  const baseLabel = policyResult.blockedByLabel || "Smart Rule";
4834
4756
  const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
4835
4757
  if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
@@ -4854,14 +4776,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4854
4776
  { ...meta, ruleName: policyResult.ruleName },
4855
4777
  hashAuditArgs
4856
4778
  );
4857
- if (approvers.cloud && creds?.apiKey)
4858
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4859
- ruleName: policyResult.ruleName,
4860
- ruleDescription: policyResult.ruleDescription,
4861
- blockedByLabel: policyResult.blockedByLabel,
4862
- matchedField: policyResult.matchedField,
4863
- matchedWord: policyResult.matchedWord
4864
- });
4865
4779
  return {
4866
4780
  approved: false,
4867
4781
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -4890,8 +4804,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4890
4804
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
4891
4805
  const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
4892
4806
  if (persistent === "allow") {
4893
- if (approvers.cloud && creds?.apiKey)
4894
- await auditLocalAllow(toolName, args, "persistent", creds, meta);
4895
4807
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
4896
4808
  return { approved: true, checkedBy: "persistent" };
4897
4809
  }
@@ -4924,8 +4836,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4924
4836
  }
4925
4837
  }
4926
4838
  if (!taintWarning && getActiveTrustSession(toolName, args)) {
4927
- if (approvers.cloud && creds?.apiKey)
4928
- await auditLocalAllow(toolName, args, "trust", creds, meta);
4929
4839
  if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
4930
4840
  return { approved: true, checkedBy: "trust" };
4931
4841
  }
@@ -5170,7 +5080,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
5170
5080
  args,
5171
5081
  finalResult.approved ? "allow" : "deny",
5172
5082
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
5173
- meta,
5083
+ // cloudRequestId links this row to the BE-origin AuditLog row the
5084
+ // /intercept handshake created — the shipper hands it to the SaaS so
5085
+ // the BE enriches that row instead of inserting a duplicate. Matters
5086
+ // for EVERY racer outcome, not just cloud wins: a native-popup
5087
+ // decision on a cloud-pending request would otherwise count twice.
5088
+ cloudRequestId ? { ...meta, cloudRequestId } : meta,
5174
5089
  hashAuditArgs
5175
5090
  );
5176
5091
  }