@node9/proxy 2.9.0 → 2.9.2

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 CHANGED
@@ -235,30 +235,86 @@ var init_audit = __esm({
235
235
  });
236
236
 
237
237
  // src/config-schema.ts
238
+ function formatIssues(issues) {
239
+ const lines = issues.map((issue) => {
240
+ const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
241
+ return ` \u2022 ${path77}: ${issue.message}`;
242
+ });
243
+ return `Invalid config:
244
+ ${lines.join("\n")}`;
245
+ }
246
+ function prunePaths(root, paths) {
247
+ let removed = false;
248
+ const ordered = [...paths].sort((x, y) => {
249
+ if (y.length !== x.length) return y.length - x.length;
250
+ const xi = x[x.length - 1];
251
+ const yi = y[y.length - 1];
252
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
253
+ });
254
+ for (const path77 of ordered) {
255
+ let cur = root;
256
+ for (const key of path77.slice(0, -1)) {
257
+ if (cur === null || typeof cur !== "object") {
258
+ cur = void 0;
259
+ break;
260
+ }
261
+ cur = cur[key];
262
+ }
263
+ if (cur === null || typeof cur !== "object") continue;
264
+ const last = path77[path77.length - 1];
265
+ if (Array.isArray(cur)) {
266
+ const i = Number(last);
267
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
268
+ cur.splice(i, 1);
269
+ removed = true;
270
+ }
271
+ continue;
272
+ }
273
+ const obj = cur;
274
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
275
+ delete obj[String(last)];
276
+ removed = true;
277
+ }
278
+ }
279
+ return removed;
280
+ }
238
281
  function sanitizeConfig(raw) {
239
282
  const result = ConfigFileSchema.safeParse(raw);
240
283
  if (result.success) {
241
284
  return { sanitized: result.data, error: null };
242
285
  }
243
- const invalidTopLevelKeys = new Set(
244
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
245
- );
246
- const sanitized = {};
247
- if (typeof raw === "object" && raw !== null) {
248
- for (const [key, value] of Object.entries(raw)) {
249
- if (!invalidTopLevelKeys.has(key)) {
250
- sanitized[key] = value;
251
- }
286
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
287
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
288
+ }
289
+ const working = structuredClone(raw);
290
+ for (let level = 0; level < 6; level++) {
291
+ for (let pass = 0; pass < 5; pass++) {
292
+ const attempt = ConfigFileSchema.safeParse(working);
293
+ if (attempt.success) break;
294
+ const paths = attempt.error.issues.flatMap((issue) => {
295
+ const at = issue.path;
296
+ if (issue.code === "unrecognized_keys") {
297
+ return issue.keys.map((k) => [...at, k]);
298
+ }
299
+ return [at.slice(0, -level || void 0)];
300
+ }).filter((path77) => path77.length > 0);
301
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
252
302
  }
303
+ if (ConfigFileSchema.safeParse(working).success) break;
304
+ }
305
+ const after = ConfigFileSchema.safeParse(working);
306
+ const sanitized = {};
307
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
308
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
309
+ );
310
+ for (const [key, value] of Object.entries(working)) {
311
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
253
312
  }
254
- const lines = result.error.issues.map((issue) => {
255
- const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
256
- return ` \u2022 ${path77}: ${issue.message}`;
257
- });
258
313
  return {
259
314
  sanitized,
260
- error: `Invalid config:
261
- ${lines.join("\n")}`
315
+ // The message names what the USER should fix, so it is built from the
316
+ // original parse, not from whatever survived the prune.
317
+ error: formatIssues(result.error.issues)
262
318
  };
263
319
  }
264
320
  var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -283,9 +339,9 @@ var init_config_schema = __esm({
283
339
  "notMatchesGlob"
284
340
  ],
285
341
  {
286
- errorMap: () => ({
287
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
288
- })
342
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
343
+ // it is what a user sees when their config is rejected.
344
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
289
345
  }
290
346
  ),
291
347
  value: import_zod.z.string().optional(),
@@ -303,7 +359,7 @@ var init_config_schema = __esm({
303
359
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
304
360
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
305
361
  verdict: import_zod.z.enum(["allow", "review", "block"], {
306
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
362
+ error: () => "verdict must be one of: allow, review, block"
307
363
  }),
308
364
  reason: import_zod.z.string().optional(),
309
365
  description: import_zod.z.string().optional(),
@@ -376,7 +432,7 @@ var init_config_schema = __esm({
376
432
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
377
433
  dangerousWords: import_zod.z.array(noNewlines).optional(),
378
434
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
379
- toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
435
+ toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
380
436
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
381
437
  dlp: import_zod.z.object({
382
438
  enabled: import_zod.z.boolean().optional(),
@@ -421,8 +477,8 @@ var init_config_schema = __esm({
421
477
  roots: import_zod.z.array(import_zod.z.string()).optional()
422
478
  }).optional()
423
479
  }).optional(),
424
- environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
425
- }).strict({ message: "Config contains unknown top-level keys" });
480
+ environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
481
+ }).strict();
426
482
  }
427
483
  });
428
484
 
@@ -17287,10 +17343,7 @@ function registerScanCommand(program2) {
17287
17343
  const n = scan.canaryFindings.length;
17288
17344
  console.log(
17289
17345
  " " + import_chalk6.default.red.bold("\u{1FAA4} Decoy Tripped") + import_chalk6.default.dim(" \xB7 ") + import_chalk6.default.red(
17290
- `${num(n)} decoy credential${n !== 1 ? "s" : ""} left the file node9 plantedit in`.replace(
17291
- "planted it in",
17292
- "planted it in"
17293
- )
17346
+ `${num(n)} decoy credential${n !== 1 ? "s" : ""} left the file node9 planted it in`
17294
17347
  )
17295
17348
  );
17296
17349
  const shownCanaries = drillDown ? scan.canaryFindings : scan.canaryFindings.slice(0, topN);
@@ -54430,12 +54483,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
54430
54483
  `${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
54431
54484
  );
54432
54485
  dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
54433
- dimRow(
54434
- "\u{1F6E0}",
54435
- "Tool rules",
54436
- d.toolRules.blocked > 0,
54437
- `${num2(d.toolRules.blocked)} shields/rules`
54438
- );
54486
+ dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
54439
54487
  dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
54440
54488
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
54441
54489
  }
@@ -59137,9 +59185,8 @@ var DB_USERS = ["app", "svc", "reporter"];
59137
59185
  var DB_HOSTS = ["db-internal", "pg-primary.internal", "postgres.svc.cluster.local"];
59138
59186
  var DB_NAMES = ["app", "prod", "main"];
59139
59187
  var pick = (alphabet, n) => {
59140
- const bytes = (0, import_crypto15.randomBytes)(n);
59141
59188
  let out = "";
59142
- for (let i = 0; i < n; i++) out += alphabet[bytes[i] % alphabet.length];
59189
+ for (let i = 0; i < n; i++) out += alphabet[(0, import_crypto15.randomInt)(alphabet.length)];
59143
59190
  return out;
59144
59191
  };
59145
59192
  var choose = (xs) => xs[(0, import_crypto15.randomInt)(xs.length)];
@@ -59281,9 +59328,8 @@ function plantKind(kind, home = import_os55.default.homedir()) {
59281
59328
  };
59282
59329
  }
59283
59330
  const dir = import_path61.default.dirname(target);
59284
- const createdDir = !import_fs66.default.existsSync(dir);
59285
59331
  const gen = site.generate();
59286
- if (createdDir) import_fs66.default.mkdirSync(dir, { recursive: true, mode: site.dirMode });
59332
+ const createdDir = import_fs66.default.mkdirSync(dir, { recursive: true, mode: site.dirMode }) !== void 0;
59287
59333
  import_fs66.default.writeFileSync(target, gen.text, { mode: 384, flag: "wx" });
59288
59334
  import_fs66.default.chmodSync(target, 384);
59289
59335
  const fileHash = sha256Hex(gen.text);
package/dist/cli.mjs CHANGED
@@ -240,30 +240,86 @@ var init_audit = __esm({
240
240
 
241
241
  // src/config-schema.ts
242
242
  import { z } from "zod";
243
+ function formatIssues(issues) {
244
+ const lines = issues.map((issue) => {
245
+ const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
246
+ return ` \u2022 ${path77}: ${issue.message}`;
247
+ });
248
+ return `Invalid config:
249
+ ${lines.join("\n")}`;
250
+ }
251
+ function prunePaths(root, paths) {
252
+ let removed = false;
253
+ const ordered = [...paths].sort((x, y) => {
254
+ if (y.length !== x.length) return y.length - x.length;
255
+ const xi = x[x.length - 1];
256
+ const yi = y[y.length - 1];
257
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
258
+ });
259
+ for (const path77 of ordered) {
260
+ let cur = root;
261
+ for (const key of path77.slice(0, -1)) {
262
+ if (cur === null || typeof cur !== "object") {
263
+ cur = void 0;
264
+ break;
265
+ }
266
+ cur = cur[key];
267
+ }
268
+ if (cur === null || typeof cur !== "object") continue;
269
+ const last = path77[path77.length - 1];
270
+ if (Array.isArray(cur)) {
271
+ const i = Number(last);
272
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
273
+ cur.splice(i, 1);
274
+ removed = true;
275
+ }
276
+ continue;
277
+ }
278
+ const obj = cur;
279
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
280
+ delete obj[String(last)];
281
+ removed = true;
282
+ }
283
+ }
284
+ return removed;
285
+ }
243
286
  function sanitizeConfig(raw) {
244
287
  const result = ConfigFileSchema.safeParse(raw);
245
288
  if (result.success) {
246
289
  return { sanitized: result.data, error: null };
247
290
  }
248
- const invalidTopLevelKeys = new Set(
249
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
250
- );
251
- const sanitized = {};
252
- if (typeof raw === "object" && raw !== null) {
253
- for (const [key, value] of Object.entries(raw)) {
254
- if (!invalidTopLevelKeys.has(key)) {
255
- sanitized[key] = value;
256
- }
291
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
292
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
293
+ }
294
+ const working = structuredClone(raw);
295
+ for (let level = 0; level < 6; level++) {
296
+ for (let pass = 0; pass < 5; pass++) {
297
+ const attempt = ConfigFileSchema.safeParse(working);
298
+ if (attempt.success) break;
299
+ const paths = attempt.error.issues.flatMap((issue) => {
300
+ const at = issue.path;
301
+ if (issue.code === "unrecognized_keys") {
302
+ return issue.keys.map((k) => [...at, k]);
303
+ }
304
+ return [at.slice(0, -level || void 0)];
305
+ }).filter((path77) => path77.length > 0);
306
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
257
307
  }
308
+ if (ConfigFileSchema.safeParse(working).success) break;
309
+ }
310
+ const after = ConfigFileSchema.safeParse(working);
311
+ const sanitized = {};
312
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
313
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
314
+ );
315
+ for (const [key, value] of Object.entries(working)) {
316
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
258
317
  }
259
- const lines = result.error.issues.map((issue) => {
260
- const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
261
- return ` \u2022 ${path77}: ${issue.message}`;
262
- });
263
318
  return {
264
319
  sanitized,
265
- error: `Invalid config:
266
- ${lines.join("\n")}`
320
+ // The message names what the USER should fix, so it is built from the
321
+ // original parse, not from whatever survived the prune.
322
+ error: formatIssues(result.error.issues)
267
323
  };
268
324
  }
269
325
  var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -287,9 +343,9 @@ var init_config_schema = __esm({
287
343
  "notMatchesGlob"
288
344
  ],
289
345
  {
290
- errorMap: () => ({
291
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
292
- })
346
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
347
+ // it is what a user sees when their config is rejected.
348
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
293
349
  }
294
350
  ),
295
351
  value: z.string().optional(),
@@ -307,7 +363,7 @@ var init_config_schema = __esm({
307
363
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
308
364
  conditionMode: z.enum(["all", "any"]).optional(),
309
365
  verdict: z.enum(["allow", "review", "block"], {
310
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
366
+ error: () => "verdict must be one of: allow, review, block"
311
367
  }),
312
368
  reason: z.string().optional(),
313
369
  description: z.string().optional(),
@@ -380,7 +436,7 @@ var init_config_schema = __esm({
380
436
  sandboxPaths: z.array(z.string()).optional(),
381
437
  dangerousWords: z.array(noNewlines).optional(),
382
438
  ignoredTools: z.array(z.string()).optional(),
383
- toolInspection: z.record(z.string()).optional(),
439
+ toolInspection: z.record(z.string(), z.string()).optional(),
384
440
  smartRules: z.array(SmartRuleSchema).optional(),
385
441
  dlp: z.object({
386
442
  enabled: z.boolean().optional(),
@@ -425,8 +481,8 @@ var init_config_schema = __esm({
425
481
  roots: z.array(z.string()).optional()
426
482
  }).optional()
427
483
  }).optional(),
428
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
429
- }).strict({ message: "Config contains unknown top-level keys" });
484
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
485
+ }).strict();
430
486
  }
431
487
  });
432
488
 
@@ -17291,10 +17347,7 @@ function registerScanCommand(program2) {
17291
17347
  const n = scan.canaryFindings.length;
17292
17348
  console.log(
17293
17349
  " " + chalk6.red.bold("\u{1FAA4} Decoy Tripped") + chalk6.dim(" \xB7 ") + chalk6.red(
17294
- `${num(n)} decoy credential${n !== 1 ? "s" : ""} left the file node9 plantedit in`.replace(
17295
- "planted it in",
17296
- "planted it in"
17297
- )
17350
+ `${num(n)} decoy credential${n !== 1 ? "s" : ""} left the file node9 planted it in`
17298
17351
  )
17299
17352
  );
17300
17353
  const shownCanaries = drillDown ? scan.canaryFindings : scan.canaryFindings.slice(0, topN);
@@ -54422,12 +54475,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
54422
54475
  `${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
54423
54476
  );
54424
54477
  dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
54425
- dimRow(
54426
- "\u{1F6E0}",
54427
- "Tool rules",
54428
- d.toolRules.blocked > 0,
54429
- `${num2(d.toolRules.blocked)} shields/rules`
54430
- );
54478
+ dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
54431
54479
  dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
54432
54480
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
54433
54481
  }
@@ -59113,7 +59161,7 @@ import path64 from "path";
59113
59161
 
59114
59162
  // src/canary/generate.ts
59115
59163
  init_dlp();
59116
- import { randomBytes as randomBytes2, randomInt } from "crypto";
59164
+ import { randomInt } from "crypto";
59117
59165
  var ALPHA_B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
59118
59166
  var ALPHA_ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
59119
59167
  var ALPHA_B64 = ALPHA_ALNUM + "+/";
@@ -59129,9 +59177,8 @@ var DB_USERS = ["app", "svc", "reporter"];
59129
59177
  var DB_HOSTS = ["db-internal", "pg-primary.internal", "postgres.svc.cluster.local"];
59130
59178
  var DB_NAMES = ["app", "prod", "main"];
59131
59179
  var pick = (alphabet, n) => {
59132
- const bytes = randomBytes2(n);
59133
59180
  let out = "";
59134
- for (let i = 0; i < n; i++) out += alphabet[bytes[i] % alphabet.length];
59181
+ for (let i = 0; i < n; i++) out += alphabet[randomInt(alphabet.length)];
59135
59182
  return out;
59136
59183
  };
59137
59184
  var choose = (xs) => xs[randomInt(xs.length)];
@@ -59273,9 +59320,8 @@ function plantKind(kind, home = os59.homedir()) {
59273
59320
  };
59274
59321
  }
59275
59322
  const dir = path65.dirname(target);
59276
- const createdDir = !fs70.existsSync(dir);
59277
59323
  const gen = site.generate();
59278
- if (createdDir) fs70.mkdirSync(dir, { recursive: true, mode: site.dirMode });
59324
+ const createdDir = fs70.mkdirSync(dir, { recursive: true, mode: site.dirMode }) !== void 0;
59279
59325
  fs70.writeFileSync(target, gen.text, { mode: 384, flag: "wx" });
59280
59326
  fs70.chmodSync(target, 384);
59281
59327
  const fileHash = sha256Hex(gen.text);
@@ -2737,30 +2737,86 @@ var init_daemon = __esm({
2737
2737
 
2738
2738
  // src/config-schema.ts
2739
2739
  import { z } from "zod";
2740
+ function formatIssues(issues) {
2741
+ const lines = issues.map((issue) => {
2742
+ const path14 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
2743
+ return ` \u2022 ${path14}: ${issue.message}`;
2744
+ });
2745
+ return `Invalid config:
2746
+ ${lines.join("\n")}`;
2747
+ }
2748
+ function prunePaths(root, paths) {
2749
+ let removed = false;
2750
+ const ordered = [...paths].sort((x, y) => {
2751
+ if (y.length !== x.length) return y.length - x.length;
2752
+ const xi = x[x.length - 1];
2753
+ const yi = y[y.length - 1];
2754
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
2755
+ });
2756
+ for (const path14 of ordered) {
2757
+ let cur = root;
2758
+ for (const key of path14.slice(0, -1)) {
2759
+ if (cur === null || typeof cur !== "object") {
2760
+ cur = void 0;
2761
+ break;
2762
+ }
2763
+ cur = cur[key];
2764
+ }
2765
+ if (cur === null || typeof cur !== "object") continue;
2766
+ const last = path14[path14.length - 1];
2767
+ if (Array.isArray(cur)) {
2768
+ const i = Number(last);
2769
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
2770
+ cur.splice(i, 1);
2771
+ removed = true;
2772
+ }
2773
+ continue;
2774
+ }
2775
+ const obj = cur;
2776
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
2777
+ delete obj[String(last)];
2778
+ removed = true;
2779
+ }
2780
+ }
2781
+ return removed;
2782
+ }
2740
2783
  function sanitizeConfig(raw) {
2741
2784
  const result = ConfigFileSchema.safeParse(raw);
2742
2785
  if (result.success) {
2743
2786
  return { sanitized: result.data, error: null };
2744
2787
  }
2745
- const invalidTopLevelKeys = new Set(
2746
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
2747
- );
2748
- const sanitized = {};
2749
- if (typeof raw === "object" && raw !== null) {
2750
- for (const [key, value] of Object.entries(raw)) {
2751
- if (!invalidTopLevelKeys.has(key)) {
2752
- sanitized[key] = value;
2753
- }
2788
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2789
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
2790
+ }
2791
+ const working = structuredClone(raw);
2792
+ for (let level = 0; level < 6; level++) {
2793
+ for (let pass = 0; pass < 5; pass++) {
2794
+ const attempt = ConfigFileSchema.safeParse(working);
2795
+ if (attempt.success) break;
2796
+ const paths = attempt.error.issues.flatMap((issue) => {
2797
+ const at = issue.path;
2798
+ if (issue.code === "unrecognized_keys") {
2799
+ return issue.keys.map((k) => [...at, k]);
2800
+ }
2801
+ return [at.slice(0, -level || void 0)];
2802
+ }).filter((path14) => path14.length > 0);
2803
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
2754
2804
  }
2805
+ if (ConfigFileSchema.safeParse(working).success) break;
2806
+ }
2807
+ const after = ConfigFileSchema.safeParse(working);
2808
+ const sanitized = {};
2809
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
2810
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
2811
+ );
2812
+ for (const [key, value] of Object.entries(working)) {
2813
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
2755
2814
  }
2756
- const lines = result.error.issues.map((issue) => {
2757
- const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
2758
- return ` \u2022 ${path14}: ${issue.message}`;
2759
- });
2760
2815
  return {
2761
2816
  sanitized,
2762
- error: `Invalid config:
2763
- ${lines.join("\n")}`
2817
+ // The message names what the USER should fix, so it is built from the
2818
+ // original parse, not from whatever survived the prune.
2819
+ error: formatIssues(result.error.issues)
2764
2820
  };
2765
2821
  }
2766
2822
  var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -2784,9 +2840,9 @@ var init_config_schema = __esm({
2784
2840
  "notMatchesGlob"
2785
2841
  ],
2786
2842
  {
2787
- errorMap: () => ({
2788
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
2789
- })
2843
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
2844
+ // it is what a user sees when their config is rejected.
2845
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
2790
2846
  }
2791
2847
  ),
2792
2848
  value: z.string().optional(),
@@ -2804,7 +2860,7 @@ var init_config_schema = __esm({
2804
2860
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
2805
2861
  conditionMode: z.enum(["all", "any"]).optional(),
2806
2862
  verdict: z.enum(["allow", "review", "block"], {
2807
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
2863
+ error: () => "verdict must be one of: allow, review, block"
2808
2864
  }),
2809
2865
  reason: z.string().optional(),
2810
2866
  description: z.string().optional(),
@@ -2877,7 +2933,7 @@ var init_config_schema = __esm({
2877
2933
  sandboxPaths: z.array(z.string()).optional(),
2878
2934
  dangerousWords: z.array(noNewlines).optional(),
2879
2935
  ignoredTools: z.array(z.string()).optional(),
2880
- toolInspection: z.record(z.string()).optional(),
2936
+ toolInspection: z.record(z.string(), z.string()).optional(),
2881
2937
  smartRules: z.array(SmartRuleSchema).optional(),
2882
2938
  dlp: z.object({
2883
2939
  enabled: z.boolean().optional(),
@@ -2922,8 +2978,8 @@ var init_config_schema = __esm({
2922
2978
  roots: z.array(z.string()).optional()
2923
2979
  }).optional()
2924
2980
  }).optional(),
2925
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
2926
- }).strict({ message: "Config contains unknown top-level keys" });
2981
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
2982
+ }).strict();
2927
2983
  }
2928
2984
  });
2929
2985
 
package/dist/index.js CHANGED
@@ -265,9 +265,9 @@ var SmartConditionSchema = import_zod.z.object({
265
265
  "notMatchesGlob"
266
266
  ],
267
267
  {
268
- errorMap: () => ({
269
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
270
- })
268
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
269
+ // it is what a user sees when their config is rejected.
270
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
271
271
  }
272
272
  ),
273
273
  value: import_zod.z.string().optional(),
@@ -285,7 +285,7 @@ var SmartRuleSchema = import_zod.z.object({
285
285
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
286
286
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
287
287
  verdict: import_zod.z.enum(["allow", "review", "block"], {
288
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
288
+ error: () => "verdict must be one of: allow, review, block"
289
289
  }),
290
290
  reason: import_zod.z.string().optional(),
291
291
  description: import_zod.z.string().optional(),
@@ -358,7 +358,7 @@ var ConfigFileSchema = import_zod.z.object({
358
358
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
359
359
  dangerousWords: import_zod.z.array(noNewlines).optional(),
360
360
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
361
- toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
361
+ toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
362
362
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
363
363
  dlp: import_zod.z.object({
364
364
  enabled: import_zod.z.boolean().optional(),
@@ -403,32 +403,88 @@ var ConfigFileSchema = import_zod.z.object({
403
403
  roots: import_zod.z.array(import_zod.z.string()).optional()
404
404
  }).optional()
405
405
  }).optional(),
406
- environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
407
- }).strict({ message: "Config contains unknown top-level keys" });
406
+ environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
407
+ }).strict();
408
+ function formatIssues(issues) {
409
+ const lines = issues.map((issue) => {
410
+ const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
411
+ return ` \u2022 ${path15}: ${issue.message}`;
412
+ });
413
+ return `Invalid config:
414
+ ${lines.join("\n")}`;
415
+ }
416
+ function prunePaths(root, paths) {
417
+ let removed = false;
418
+ const ordered = [...paths].sort((x, y) => {
419
+ if (y.length !== x.length) return y.length - x.length;
420
+ const xi = x[x.length - 1];
421
+ const yi = y[y.length - 1];
422
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
423
+ });
424
+ for (const path15 of ordered) {
425
+ let cur = root;
426
+ for (const key of path15.slice(0, -1)) {
427
+ if (cur === null || typeof cur !== "object") {
428
+ cur = void 0;
429
+ break;
430
+ }
431
+ cur = cur[key];
432
+ }
433
+ if (cur === null || typeof cur !== "object") continue;
434
+ const last = path15[path15.length - 1];
435
+ if (Array.isArray(cur)) {
436
+ const i = Number(last);
437
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
438
+ cur.splice(i, 1);
439
+ removed = true;
440
+ }
441
+ continue;
442
+ }
443
+ const obj = cur;
444
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
445
+ delete obj[String(last)];
446
+ removed = true;
447
+ }
448
+ }
449
+ return removed;
450
+ }
408
451
  function sanitizeConfig(raw) {
409
452
  const result = ConfigFileSchema.safeParse(raw);
410
453
  if (result.success) {
411
454
  return { sanitized: result.data, error: null };
412
455
  }
413
- const invalidTopLevelKeys = new Set(
414
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
415
- );
416
- const sanitized = {};
417
- if (typeof raw === "object" && raw !== null) {
418
- for (const [key, value] of Object.entries(raw)) {
419
- if (!invalidTopLevelKeys.has(key)) {
420
- sanitized[key] = value;
421
- }
456
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
457
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
458
+ }
459
+ const working = structuredClone(raw);
460
+ for (let level = 0; level < 6; level++) {
461
+ for (let pass = 0; pass < 5; pass++) {
462
+ const attempt = ConfigFileSchema.safeParse(working);
463
+ if (attempt.success) break;
464
+ const paths = attempt.error.issues.flatMap((issue) => {
465
+ const at = issue.path;
466
+ if (issue.code === "unrecognized_keys") {
467
+ return issue.keys.map((k) => [...at, k]);
468
+ }
469
+ return [at.slice(0, -level || void 0)];
470
+ }).filter((path15) => path15.length > 0);
471
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
422
472
  }
473
+ if (ConfigFileSchema.safeParse(working).success) break;
474
+ }
475
+ const after = ConfigFileSchema.safeParse(working);
476
+ const sanitized = {};
477
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
478
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
479
+ );
480
+ for (const [key, value] of Object.entries(working)) {
481
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
423
482
  }
424
- const lines = result.error.issues.map((issue) => {
425
- const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
426
- return ` \u2022 ${path15}: ${issue.message}`;
427
- });
428
483
  return {
429
484
  sanitized,
430
- error: `Invalid config:
431
- ${lines.join("\n")}`
485
+ // The message names what the USER should fix, so it is built from the
486
+ // original parse, not from whatever survived the prune.
487
+ error: formatIssues(result.error.issues)
432
488
  };
433
489
  }
434
490
 
package/dist/index.mjs CHANGED
@@ -235,9 +235,9 @@ var SmartConditionSchema = z.object({
235
235
  "notMatchesGlob"
236
236
  ],
237
237
  {
238
- errorMap: () => ({
239
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
240
- })
238
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
239
+ // it is what a user sees when their config is rejected.
240
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
241
241
  }
242
242
  ),
243
243
  value: z.string().optional(),
@@ -255,7 +255,7 @@ var SmartRuleSchema = z.object({
255
255
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
256
256
  conditionMode: z.enum(["all", "any"]).optional(),
257
257
  verdict: z.enum(["allow", "review", "block"], {
258
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
258
+ error: () => "verdict must be one of: allow, review, block"
259
259
  }),
260
260
  reason: z.string().optional(),
261
261
  description: z.string().optional(),
@@ -328,7 +328,7 @@ var ConfigFileSchema = z.object({
328
328
  sandboxPaths: z.array(z.string()).optional(),
329
329
  dangerousWords: z.array(noNewlines).optional(),
330
330
  ignoredTools: z.array(z.string()).optional(),
331
- toolInspection: z.record(z.string()).optional(),
331
+ toolInspection: z.record(z.string(), z.string()).optional(),
332
332
  smartRules: z.array(SmartRuleSchema).optional(),
333
333
  dlp: z.object({
334
334
  enabled: z.boolean().optional(),
@@ -373,32 +373,88 @@ var ConfigFileSchema = z.object({
373
373
  roots: z.array(z.string()).optional()
374
374
  }).optional()
375
375
  }).optional(),
376
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
377
- }).strict({ message: "Config contains unknown top-level keys" });
376
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
377
+ }).strict();
378
+ function formatIssues(issues) {
379
+ const lines = issues.map((issue) => {
380
+ const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
381
+ return ` \u2022 ${path15}: ${issue.message}`;
382
+ });
383
+ return `Invalid config:
384
+ ${lines.join("\n")}`;
385
+ }
386
+ function prunePaths(root, paths) {
387
+ let removed = false;
388
+ const ordered = [...paths].sort((x, y) => {
389
+ if (y.length !== x.length) return y.length - x.length;
390
+ const xi = x[x.length - 1];
391
+ const yi = y[y.length - 1];
392
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
393
+ });
394
+ for (const path15 of ordered) {
395
+ let cur = root;
396
+ for (const key of path15.slice(0, -1)) {
397
+ if (cur === null || typeof cur !== "object") {
398
+ cur = void 0;
399
+ break;
400
+ }
401
+ cur = cur[key];
402
+ }
403
+ if (cur === null || typeof cur !== "object") continue;
404
+ const last = path15[path15.length - 1];
405
+ if (Array.isArray(cur)) {
406
+ const i = Number(last);
407
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
408
+ cur.splice(i, 1);
409
+ removed = true;
410
+ }
411
+ continue;
412
+ }
413
+ const obj = cur;
414
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
415
+ delete obj[String(last)];
416
+ removed = true;
417
+ }
418
+ }
419
+ return removed;
420
+ }
378
421
  function sanitizeConfig(raw) {
379
422
  const result = ConfigFileSchema.safeParse(raw);
380
423
  if (result.success) {
381
424
  return { sanitized: result.data, error: null };
382
425
  }
383
- const invalidTopLevelKeys = new Set(
384
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
385
- );
386
- const sanitized = {};
387
- if (typeof raw === "object" && raw !== null) {
388
- for (const [key, value] of Object.entries(raw)) {
389
- if (!invalidTopLevelKeys.has(key)) {
390
- sanitized[key] = value;
391
- }
426
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
427
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
428
+ }
429
+ const working = structuredClone(raw);
430
+ for (let level = 0; level < 6; level++) {
431
+ for (let pass = 0; pass < 5; pass++) {
432
+ const attempt = ConfigFileSchema.safeParse(working);
433
+ if (attempt.success) break;
434
+ const paths = attempt.error.issues.flatMap((issue) => {
435
+ const at = issue.path;
436
+ if (issue.code === "unrecognized_keys") {
437
+ return issue.keys.map((k) => [...at, k]);
438
+ }
439
+ return [at.slice(0, -level || void 0)];
440
+ }).filter((path15) => path15.length > 0);
441
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
392
442
  }
443
+ if (ConfigFileSchema.safeParse(working).success) break;
444
+ }
445
+ const after = ConfigFileSchema.safeParse(working);
446
+ const sanitized = {};
447
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
448
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
449
+ );
450
+ for (const [key, value] of Object.entries(working)) {
451
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
393
452
  }
394
- const lines = result.error.issues.map((issue) => {
395
- const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
396
- return ` \u2022 ${path15}: ${issue.message}`;
397
- });
398
453
  return {
399
454
  sanitized,
400
- error: `Invalid config:
401
- ${lines.join("\n")}`
455
+ // The message names what the USER should fix, so it is built from the
456
+ // original parse, not from whatever survived the prune.
457
+ error: formatIssues(result.error.issues)
402
458
  };
403
459
  }
404
460
 
package/dist/scan-ink.mjs CHANGED
@@ -61,9 +61,9 @@ var SmartConditionSchema = z.object({
61
61
  "notMatchesGlob"
62
62
  ],
63
63
  {
64
- errorMap: () => ({
65
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
66
- })
64
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
65
+ // it is what a user sees when their config is rejected.
66
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
67
67
  }
68
68
  ),
69
69
  value: z.string().optional(),
@@ -81,7 +81,7 @@ var SmartRuleSchema = z.object({
81
81
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
82
82
  conditionMode: z.enum(["all", "any"]).optional(),
83
83
  verdict: z.enum(["allow", "review", "block"], {
84
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
84
+ error: () => "verdict must be one of: allow, review, block"
85
85
  }),
86
86
  reason: z.string().optional(),
87
87
  description: z.string().optional(),
@@ -154,7 +154,7 @@ var ConfigFileSchema = z.object({
154
154
  sandboxPaths: z.array(z.string()).optional(),
155
155
  dangerousWords: z.array(noNewlines).optional(),
156
156
  ignoredTools: z.array(z.string()).optional(),
157
- toolInspection: z.record(z.string()).optional(),
157
+ toolInspection: z.record(z.string(), z.string()).optional(),
158
158
  smartRules: z.array(SmartRuleSchema).optional(),
159
159
  dlp: z.object({
160
160
  enabled: z.boolean().optional(),
@@ -199,8 +199,8 @@ var ConfigFileSchema = z.object({
199
199
  roots: z.array(z.string()).optional()
200
200
  }).optional()
201
201
  }).optional(),
202
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
203
- }).strict({ message: "Config contains unknown top-level keys" });
202
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
203
+ }).strict();
204
204
 
205
205
  // src/shields.ts
206
206
  import fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -94,23 +94,23 @@
94
94
  "smol-toml": "^1.6.1",
95
95
  "string-width": "^4.2.3",
96
96
  "yaml": "^2.9.0",
97
- "zod": "^3.25.76"
97
+ "zod": "^4.5.4"
98
98
  },
99
99
  "bundleDependencies": [
100
100
  "mvdan-sh"
101
101
  ],
102
102
  "devDependencies": {
103
- "@anthropic-ai/sdk": "^0.78.0",
103
+ "@anthropic-ai/sdk": "^0.124.0",
104
104
  "@octokit/rest": "^22.0.1",
105
105
  "@semantic-release/commit-analyzer": "^13.0.1",
106
106
  "@semantic-release/git": "^10.0.1",
107
107
  "@semantic-release/github": "^12.0.6",
108
108
  "@semantic-release/npm": "^13.1.5",
109
109
  "@semantic-release/release-notes-generator": "^14.1.0",
110
- "@types/node": "^25.3.1",
110
+ "@types/node": "^26.4.1",
111
111
  "@types/picomatch": "^4.0.2",
112
112
  "@types/react": "^19.2.14",
113
- "@vitest/coverage-v8": "4.1.2",
113
+ "@vitest/coverage-v8": "^5.0.0",
114
114
  "cross-env": "^10.1.0",
115
115
  "ink-testing-library": "^4.0.0",
116
116
  "prettier": "^3.4.2",
@@ -119,7 +119,7 @@
119
119
  "tsx": "^4.21.0",
120
120
  "typescript": "^5.9.3",
121
121
  "typescript-eslint": "^8.20.0",
122
- "vitest": "4.1.2"
122
+ "vitest": "^5.0.0"
123
123
  },
124
124
  "overrides": {
125
125
  "undici": "^7.24.0",