@node9/proxy 2.27.1 → 2.28.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.mjs CHANGED
@@ -364,9 +364,9 @@ function matchesPattern(text, patterns) {
364
364
  const withoutDotSlash = text.replace(/^\.\//, "");
365
365
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
366
366
  }
367
- function getNestedValue(obj, path17) {
367
+ function getNestedValue(obj, path19) {
368
368
  if (!obj || typeof obj !== "object") return null;
369
- const segments2 = path17.split(".");
369
+ const segments2 = path19.split(".");
370
370
  for (const seg of segments2) {
371
371
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
372
372
  }
@@ -2176,14 +2176,54 @@ function pipInstall(words, from, manager, out) {
2176
2176
  if (v >= 0 && lower(words[v]) === "install")
2177
2177
  pushPy(out, manager, operands(words, v + 1, PIP_VALUE_FLAGS));
2178
2178
  }
2179
+ function localContexts(file) {
2180
+ const contexts = /* @__PURE__ */ new Map();
2181
+ const sample = parseShared("cd . && true");
2182
+ if (typeof sample === "symbol") return contexts;
2183
+ const andOp = sample.Stmts[0].Cmd.Op;
2184
+ function visit(stmt, steps, depth = 0) {
2185
+ if (depth > 64) return null;
2186
+ if (!stmt || stmt.Background || stmt.Negated || stmt.Redirs?.length) return null;
2187
+ const cmd = stmt.Cmd;
2188
+ const kind = syntax2.NodeType(cmd);
2189
+ if (kind === "BinaryCmd" && cmd.Op === andOp) {
2190
+ const next = visit(cmd.X, steps, depth + 1);
2191
+ return next === null ? null : visit(cmd.Y, next, depth + 1);
2192
+ }
2193
+ if (kind !== "CallExpr" || cmd.Assigns?.length) return null;
2194
+ const words = (cmd.Args ?? []).map((w) => resolveWordLiteral(w));
2195
+ if (words[0] === "cd" && words.length === 2 && typeof words[1] === "string" && words[1] && !/^[-~]/.test(words[1]))
2196
+ return [...steps, words[1]];
2197
+ if (!["npx", "npm", "bunx", "bun"].includes(words[0])) return null;
2198
+ if (words.some(
2199
+ (w) => w === null || /^(?:--(?:prefix|cwd|directory|workspace|workspaces|global|ignore-existing)(?:=|$)|-[Cwg])/.test(
2200
+ w
2201
+ )
2202
+ ))
2203
+ return null;
2204
+ contexts.set(cmd.Pos().Offset(), steps);
2205
+ return null;
2206
+ }
2207
+ if (file.Stmts?.length === 1) visit(file.Stmts[0], []);
2208
+ return contexts;
2209
+ }
2179
2210
  function collect(command, out, depth) {
2180
2211
  if (!command || command.length > 5e4) return;
2181
2212
  const f = parseShared(command);
2182
2213
  if (typeof f === "symbol") return;
2214
+ const contexts = depth === 0 ? localContexts(f) : /* @__PURE__ */ new Map();
2183
2215
  syntax2.Walk(f, (node) => {
2184
2216
  if (node && syntax2.NodeType(node) === "CallExpr") {
2185
2217
  const words = (node.Args || []).map((a) => resolveWordLiteral(a));
2218
+ const start = out.length;
2186
2219
  if (words.length > 0) fromCall(words, out, depth);
2220
+ const steps = contexts.get(node.Pos().Offset());
2221
+ for (let i = start; i < out.length; i++) {
2222
+ if (["npx", "bunx", "npm exec"].includes(out[i].manager)) {
2223
+ if (!steps) out[i].localCwd = null;
2224
+ else if (steps.length) out[i].localCwd = steps;
2225
+ }
2226
+ }
2187
2227
  }
2188
2228
  return true;
2189
2229
  });
@@ -2193,7 +2233,7 @@ function extractPackageInstalls(command) {
2193
2233
  collect(command, out, 0);
2194
2234
  const seen = /* @__PURE__ */ new Set();
2195
2235
  return out.filter((r) => {
2196
- const k = `${r.ecosystem}\0${r.name}\0${r.version ?? ""}`;
2236
+ const k = JSON.stringify([r.ecosystem, r.manager, r.name, r.raw, r.localCwd]);
2197
2237
  if (seen.has(k)) return false;
2198
2238
  seen.add(k);
2199
2239
  return true;
@@ -2626,7 +2666,15 @@ function tuningToLegacyPolicy(policy, tuning) {
2626
2666
  const unknown = [];
2627
2667
  for (const [id, fields] of Object.entries(tuning)) {
2628
2668
  if (!fields || typeof fields !== "object") continue;
2629
- for (const [name, value] of Object.entries(fields)) {
2669
+ const normalized = { ...fields };
2670
+ if (id === "loading.malicious-package") {
2671
+ if (normalized.registrySignals === false) {
2672
+ normalized.newPackage ??= "off";
2673
+ normalized.installScript ??= "off";
2674
+ }
2675
+ delete normalized.registrySignals;
2676
+ }
2677
+ for (const [name, value] of Object.entries(normalized)) {
2630
2678
  const field = TUNING_FIELDS.find((f) => f.checkId === id && f.name === name);
2631
2679
  if (!field) {
2632
2680
  unknown.push(`${id}.${name}`);
@@ -2985,9 +3033,9 @@ function bareToolName(toolName) {
2985
3033
  const parts = toolName.split("__");
2986
3034
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
2987
3035
  }
2988
- function valuesAt(args, path17) {
3036
+ function valuesAt(args, path19) {
2989
3037
  let cursors = [args];
2990
- for (const rawSegment of path17.split(".")) {
3038
+ for (const rawSegment of path19.split(".")) {
2991
3039
  const isArray = rawSegment.endsWith("[]");
2992
3040
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
2993
3041
  const next = [];
@@ -3019,8 +3067,8 @@ function extractToolDestinations(toolName, args) {
3019
3067
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3020
3068
  if (!paths) return [];
3021
3069
  const out = [];
3022
- for (const path17 of paths) {
3023
- for (const value of valuesAt(args, path17)) {
3070
+ for (const path19 of paths) {
3071
+ for (const value of valuesAt(args, path19)) {
3024
3072
  const host = hostOf(value);
3025
3073
  if (host) out.push({ host, binary: toolName, raw: value });
3026
3074
  }
@@ -3034,15 +3082,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
3034
3082
  try {
3035
3083
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3036
3084
  if (!paths) return null;
3037
- for (const path17 of paths) {
3038
- for (const value of valuesAt(args, path17)) {
3085
+ for (const path19 of paths) {
3086
+ for (const value of valuesAt(args, path19)) {
3039
3087
  const host = hostOf(value);
3040
3088
  if (!host) continue;
3041
3089
  const m = classifySsrf(host);
3042
3090
  if (!m) continue;
3043
3091
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3044
3092
  if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
3045
- return { ...m, argPath: path17, host, reason: ssrfReason(m, host) };
3093
+ return { ...m, argPath: path19, host, reason: ssrfReason(m, host) };
3046
3094
  }
3047
3095
  }
3048
3096
  return null;
@@ -6940,14 +6988,19 @@ var init_dist = __esm({
6940
6988
  { checkId: "loading.skill-tamper", name: "roots", legacy: ["skillPinning", "roots"] },
6941
6989
  {
6942
6990
  checkId: "loading.malicious-package",
6943
- name: "registrySignals",
6944
- legacy: ["packageCheck", "registrySignals"]
6991
+ name: "newPackage",
6992
+ legacy: ["packageCheck", "newPackage"]
6945
6993
  },
6946
6994
  {
6947
6995
  checkId: "loading.malicious-package",
6948
6996
  name: "maxAgeHours",
6949
6997
  legacy: ["packageCheck", "maxAgeHours"]
6950
6998
  },
6999
+ {
7000
+ checkId: "loading.malicious-package",
7001
+ name: "installScript",
7002
+ legacy: ["packageCheck", "installScript"]
7003
+ },
6951
7004
  {
6952
7005
  checkId: "loading.malicious-package",
6953
7006
  name: "onlineFallback",
@@ -7230,7 +7283,8 @@ __export(audit_exports, {
7230
7283
  buildArgsPreview: () => buildArgsPreview,
7231
7284
  filePathFromArgs: () => filePathFromArgs,
7232
7285
  generateEventId: () => generateEventId,
7233
- redactSecrets: () => redactSecrets
7286
+ redactSecrets: () => redactSecrets,
7287
+ truncateAuditPreview: () => truncateAuditPreview
7234
7288
  });
7235
7289
  import fs from "fs";
7236
7290
  import path from "path";
@@ -7263,11 +7317,14 @@ function buildArgsPreview(args) {
7263
7317
  const primary = o && (o.command ?? o.file_path ?? o.path ?? o.url ?? o.query);
7264
7318
  const text = typeof primary === "string" ? primary : args ? JSON.stringify(args) : "";
7265
7319
  if (!text) return void 0;
7266
- return redactSecrets(text).slice(0, 120);
7320
+ return truncateAuditPreview(redactSecrets(text));
7267
7321
  } catch {
7268
7322
  return void 0;
7269
7323
  }
7270
7324
  }
7325
+ function truncateAuditPreview(text) {
7326
+ return text.slice(0, 120).replace(/[\uD800-\uDBFF]$/, "");
7327
+ }
7271
7328
  function appendToLog(logPath, entry) {
7272
7329
  try {
7273
7330
  const dir = path.dirname(logPath);
@@ -7550,7 +7607,10 @@ var ConfigFileSchema = z.object({
7550
7607
  packageCheck: z.object({
7551
7608
  enabled: z.boolean().optional(),
7552
7609
  onMalicious: z.enum(["block", "review"]).optional(),
7610
+ // 2.27.0 shape, still accepted: false = newPackage and installScript off.
7553
7611
  registrySignals: z.boolean().optional(),
7612
+ newPackage: z.enum(["review", "off"]).optional(),
7613
+ installScript: z.enum(["review", "off"]).optional(),
7554
7614
  maxAgeHours: z.number().min(0).optional(),
7555
7615
  onlineFallback: z.boolean().optional(),
7556
7616
  allow: z.array(z.string()).optional()
@@ -7565,8 +7625,8 @@ var ConfigFileSchema = z.object({
7565
7625
  }).strict();
7566
7626
  function formatIssues(issues) {
7567
7627
  const lines = issues.map((issue) => {
7568
- const path17 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
7569
- return ` \u2022 ${path17}: ${issue.message}`;
7628
+ const path19 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
7629
+ return ` \u2022 ${path19}: ${issue.message}`;
7570
7630
  });
7571
7631
  return `Invalid config:
7572
7632
  ${lines.join("\n")}`;
@@ -7579,9 +7639,9 @@ function prunePaths(root, paths) {
7579
7639
  const yi = y[y.length - 1];
7580
7640
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
7581
7641
  });
7582
- for (const path17 of ordered) {
7642
+ for (const path19 of ordered) {
7583
7643
  let cur = root;
7584
- for (const key of path17.slice(0, -1)) {
7644
+ for (const key of path19.slice(0, -1)) {
7585
7645
  if (cur === null || typeof cur !== "object") {
7586
7646
  cur = void 0;
7587
7647
  break;
@@ -7589,7 +7649,7 @@ function prunePaths(root, paths) {
7589
7649
  cur = cur[key];
7590
7650
  }
7591
7651
  if (cur === null || typeof cur !== "object") continue;
7592
- const last = path17[path17.length - 1];
7652
+ const last = path19[path19.length - 1];
7593
7653
  if (Array.isArray(cur)) {
7594
7654
  const i = Number(last);
7595
7655
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -7625,7 +7685,7 @@ function sanitizeConfig(raw) {
7625
7685
  return issue.keys.map((k) => [...at, k]);
7626
7686
  }
7627
7687
  return [at.slice(0, -level || void 0)];
7628
- }).filter((path17) => path17.length > 0);
7688
+ }).filter((path19) => path19.length > 0);
7629
7689
  if (paths.length === 0 || !prunePaths(working, paths)) break;
7630
7690
  }
7631
7691
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -8361,8 +8421,9 @@ var DEFAULT_CONFIG = {
8361
8421
  packageCheck: {
8362
8422
  enabled: true,
8363
8423
  onMalicious: "block",
8364
- registrySignals: true,
8424
+ newPackage: "review",
8365
8425
  maxAgeHours: 48,
8426
+ installScript: "review",
8366
8427
  onlineFallback: true,
8367
8428
  allow: []
8368
8429
  },
@@ -8724,7 +8785,13 @@ function getConfig(cwd) {
8724
8785
  if (typeof pc.enabled === "boolean") cur.enabled = pc.enabled;
8725
8786
  if (pc.onMalicious === "block" || pc.onMalicious === "review")
8726
8787
  cur.onMalicious = pc.onMalicious;
8727
- if (typeof pc.registrySignals === "boolean") cur.registrySignals = pc.registrySignals;
8788
+ if (pc.registrySignals === false) {
8789
+ cur.newPackage = "off";
8790
+ cur.installScript = "off";
8791
+ }
8792
+ if (pc.newPackage === "review" || pc.newPackage === "off") cur.newPackage = pc.newPackage;
8793
+ if (pc.installScript === "review" || pc.installScript === "off")
8794
+ cur.installScript = pc.installScript;
8728
8795
  if (typeof pc.maxAgeHours === "number" && pc.maxAgeHours >= 0)
8729
8796
  cur.maxAgeHours = pc.maxAgeHours;
8730
8797
  if (typeof pc.onlineFallback === "boolean") cur.onlineFallback = pc.onlineFallback;
@@ -8934,7 +9001,10 @@ function getConfig(cwd) {
8934
9001
  mergedPolicy.packageCheck = {
8935
9002
  enabled: typeof pc.enabled === "boolean" ? pc.enabled : cur.enabled,
8936
9003
  onMalicious: pc.onMalicious === "block" || pc.onMalicious === "review" ? pc.onMalicious : cur.onMalicious,
8937
- registrySignals: typeof pc.registrySignals === "boolean" ? pc.registrySignals : cur.registrySignals,
9004
+ // The 2.27.0 boolean still arrives from older dashboard columns:
9005
+ // false turns both signals off unless the new keys say otherwise.
9006
+ newPackage: pc.newPackage === "review" || pc.newPackage === "off" ? pc.newPackage : pc.registrySignals === false ? "off" : cur.newPackage,
9007
+ installScript: pc.installScript === "review" || pc.installScript === "off" ? pc.installScript : pc.registrySignals === false ? "off" : cur.installScript,
8938
9008
  maxAgeHours: typeof pc.maxAgeHours === "number" && Number.isFinite(pc.maxAgeHours) && pc.maxAgeHours >= 0 ? pc.maxAgeHours : cur.maxAgeHours,
8939
9009
  onlineFallback: typeof pc.onlineFallback === "boolean" ? pc.onlineFallback : cur.onlineFallback,
8940
9010
  allow: Array.isArray(pc.allow) ? pc.allow.filter((a) => typeof a === "string" && a.length > 0) : cur.allow
@@ -8960,13 +9030,13 @@ function getConfig(cwd) {
8960
9030
  }
8961
9031
  if (Array.isArray(mc.jailPaths)) {
8962
9032
  for (const jp of mc.jailPaths) {
8963
- const path17 = typeof jp?.path === "string" ? jp.path.trim() : "";
8964
- if (!path17) continue;
9033
+ const path19 = typeof jp?.path === "string" ? jp.path.trim() : "";
9034
+ if (!path19) continue;
8965
9035
  const verdict = jp?.verdict === "review" ? "review" : "block";
8966
- for (const r of pathRules(path17, verdict, "org-managed jail")) {
9036
+ for (const r of pathRules(path19, verdict, "org-managed jail")) {
8967
9037
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
8968
9038
  }
8969
- mergedPolicy.managedJailPaths.push({ path: path17, verdict });
9039
+ mergedPolicy.managedJailPaths.push({ path: path19, verdict });
8970
9040
  }
8971
9041
  }
8972
9042
  if (Array.isArray(mc.trustedHosts)) {
@@ -10363,6 +10433,7 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
10363
10433
  // src/supply-chain/check.ts
10364
10434
  init_dist();
10365
10435
  import picomatch from "picomatch";
10436
+ import path18 from "path";
10366
10437
 
10367
10438
  // src/supply-chain/osv-index.ts
10368
10439
  init_dist();
@@ -10523,30 +10594,58 @@ async function queryOsvMalicious(queries) {
10523
10594
  }
10524
10595
 
10525
10596
  // src/supply-chain/registry.ts
10597
+ import { maxSatisfying, satisfies, validRange } from "semver";
10526
10598
  var TIMEOUT_MS2 = 1500;
10527
10599
  var MAX_BYTES = 2 * 1024 * 1024;
10528
- async function npmInfo(name, version) {
10600
+ async function npmInfo(name, version, freshWindowMs, raw) {
10529
10601
  const base = endpoints().npmRegistry;
10530
10602
  if (!base) return null;
10531
- const doc = await fetchJson(`${base}/${name.replaceAll("/", "%2F")}`, {
10603
+ const url = `${base}/${name.replaceAll("/", "%2F")}`;
10604
+ const doc = await fetchJson(url, {
10532
10605
  timeoutMs: TIMEOUT_MS2,
10533
10606
  maxBytes: MAX_BYTES,
10534
10607
  headers: { Accept: "application/vnd.npm.install-v1+json" }
10535
10608
  });
10536
10609
  if (!doc) return null;
10537
10610
  const latest = doc["dist-tags"]?.latest;
10538
- const resolved = version ?? latest;
10611
+ const aliasStart = raw?.indexOf("@npm:") ?? -1;
10612
+ const target = aliasStart > 0 ? raw.slice(aliasStart + 5) : raw?.replace(/^npm:/, "");
10613
+ const specStart = target?.indexOf("@", 1) ?? -1;
10614
+ const spec = specStart > 0 ? target.slice(specStart + 1) : "";
10615
+ let resolved = version;
10616
+ if (!resolved) {
10617
+ if (!spec) resolved = latest;
10618
+ else if (doc["dist-tags"]?.[spec]) resolved = doc["dist-tags"][spec];
10619
+ else if (validRange(spec)) {
10620
+ resolved = latest && satisfies(latest, spec) ? latest : maxSatisfying(Object.keys(doc.versions ?? {}), spec) ?? void 0;
10621
+ }
10622
+ }
10539
10623
  if (!resolved) return null;
10540
10624
  const v = doc.versions?.[resolved];
10541
10625
  const modifiedMs = doc.modified ? Date.parse(doc.modified) : NaN;
10542
- return {
10626
+ const info = {
10543
10627
  version: resolved,
10544
- // `modified` bounds the publish time of EVERY version from above, but it
10545
- // only approximates the age of the newest one: an older pinned version
10546
- // gets no age (it is at least as old as `latest`, so never "new").
10547
- publishedAtMs: Number.isFinite(modifiedMs) && resolved === latest ? modifiedMs : void 0,
10548
10628
  hasInstallScript: v?.hasInstallScript === true
10549
10629
  };
10630
+ if (!Number.isFinite(modifiedMs)) return info;
10631
+ if (Date.now() - modifiedMs >= freshWindowMs) {
10632
+ info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
10633
+ return info;
10634
+ }
10635
+ try {
10636
+ const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
10637
+ const t = full?.time?.[resolved];
10638
+ const tMs = t ? Date.parse(t) : NaN;
10639
+ if (Number.isFinite(tMs)) {
10640
+ info.publishedAtMs = tMs;
10641
+ return info;
10642
+ }
10643
+ info.ageFallback = `no publish time for ${resolved} in the full document`;
10644
+ } catch (err) {
10645
+ info.ageFallback = `full document ${err.message}`;
10646
+ }
10647
+ info.publishedAtMs = modifiedMs;
10648
+ return info;
10550
10649
  }
10551
10650
  async function pypiInfo(name, version) {
10552
10651
  const base = endpoints().pypi;
@@ -10561,8 +10660,36 @@ async function pypiInfo(name, version) {
10561
10660
  publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
10562
10661
  };
10563
10662
  }
10564
- async function registryInfo(eco, name, version) {
10565
- return eco === "npm" ? npmInfo(name, version) : pypiInfo(name, version);
10663
+ async function registryInfo(eco, name, version, freshWindowMs, raw) {
10664
+ return eco === "npm" ? npmInfo(name, version, freshWindowMs, raw) : pypiInfo(name, version);
10665
+ }
10666
+
10667
+ // src/supply-chain/local-resolve.ts
10668
+ import fs16 from "fs";
10669
+ import path17 from "path";
10670
+ var MAX_LEVELS = 32;
10671
+ var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
10672
+ function resolveInstalled(name, cwd) {
10673
+ if (!cwd || !path17.isAbsolute(cwd)) return null;
10674
+ if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
10675
+ let dir = path17.resolve(cwd);
10676
+ for (let i = 0; i < MAX_LEVELS; i++) {
10677
+ const pkgDir = path17.join(dir, "node_modules", ...name.split("/"));
10678
+ const manifest = path17.join(pkgDir, "package.json");
10679
+ try {
10680
+ const raw = fs16.readFileSync(manifest, "utf8");
10681
+ const parsed = JSON.parse(raw);
10682
+ if (typeof parsed.version === "string" && parsed.version.length > 0) {
10683
+ return { version: parsed.version, dir: pkgDir };
10684
+ }
10685
+ return null;
10686
+ } catch {
10687
+ }
10688
+ const parent = path17.dirname(dir);
10689
+ if (parent === dir) break;
10690
+ dir = parent;
10691
+ }
10692
+ return null;
10566
10693
  }
10567
10694
 
10568
10695
  // src/supply-chain/check.ts
@@ -10633,13 +10760,45 @@ async function maliciousFor(p, version, cfg, misses, networkCapped) {
10633
10760
  }
10634
10761
  return null;
10635
10762
  }
10636
- async function checkOne(p, cfg, misses, networkCapped) {
10763
+ function checkInstalled(p, version, misses) {
10764
+ const local = lookupIndex(p.ecosystem, p.name);
10765
+ if (local.status === "unavailable") {
10766
+ misses.push(`${label(p, version)}: installed copy, local index unavailable`);
10767
+ return [];
10768
+ }
10769
+ if (local.status !== "hit") return [];
10770
+ const covering = local.entries.filter((e) => entryCovers(e, version) === true).map((e) => e.id);
10771
+ if (covering.length === 0) return [];
10772
+ return [
10773
+ {
10774
+ pkg: p,
10775
+ version,
10776
+ installed: true,
10777
+ kind: "malicious",
10778
+ advisories: covering,
10779
+ detail: `installed ${label(p, version)} (${covering.join(", ")})`
10780
+ }
10781
+ ];
10782
+ }
10783
+ async function checkOne(p, cfg, misses, networkCapped, cwd) {
10784
+ if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
10785
+ const localCwd = cwd && path18.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir, step) => path18.resolve(dir, step), cwd) : void 0;
10786
+ const installed = resolveInstalled(p.name, localCwd);
10787
+ if (installed && (!p.version || p.version === installed.version))
10788
+ return checkInstalled(p, installed.version, misses);
10789
+ }
10637
10790
  const findings = [];
10638
10791
  let info = null;
10639
- if (cfg.registrySignals && !networkCapped) {
10792
+ const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
10793
+ if (wantsRegistry && !networkCapped) {
10640
10794
  try {
10641
- info = await registryInfo(p.ecosystem, p.name, p.version);
10795
+ const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
10796
+ info = await registryInfo(p.ecosystem, p.name, p.version, windowMs, p.raw);
10642
10797
  if (!info) misses.push(`${label(p, p.version)}: registry metadata unavailable`);
10798
+ else if (info.ageFallback)
10799
+ misses.push(
10800
+ `${label(p, info.version)}: age from the package modified time (${info.ageFallback})`
10801
+ );
10643
10802
  } catch (err) {
10644
10803
  misses.push(`${label(p, p.version)}: registry ${err.message}`);
10645
10804
  }
@@ -10647,18 +10806,18 @@ async function checkOne(p, cfg, misses, networkCapped) {
10647
10806
  const version = p.version ?? info?.version;
10648
10807
  const mal = await maliciousFor(p, version, cfg, misses, networkCapped);
10649
10808
  if (mal) findings.push(mal);
10650
- if (info?.publishedAtMs !== void 0) {
10809
+ if (cfg.newPackage !== "off" && info?.publishedAtMs !== void 0) {
10651
10810
  const ageH = (Date.now() - info.publishedAtMs) / 36e5;
10652
10811
  if (ageH >= 0 && ageH < cfg.maxAgeHours) {
10653
10812
  findings.push({
10654
10813
  pkg: p,
10655
10814
  version,
10656
10815
  kind: "new",
10657
- detail: `${label(p, version)} was published ${Math.max(1, Math.round(ageH))}h ago (under ${cfg.maxAgeHours}h)`
10816
+ detail: info.ageFallback ? `${label(p, version)} has package metadata updated ${Math.max(1, Math.round(ageH))}h ago (under ${cfg.maxAgeHours}h); version publish time unavailable` : `${label(p, version)} was published ${Math.max(1, Math.round(ageH))}h ago (under ${cfg.maxAgeHours}h)`
10658
10817
  });
10659
10818
  }
10660
10819
  }
10661
- if (info?.hasInstallScript) {
10820
+ if (cfg.installScript !== "off" && info?.hasInstallScript) {
10662
10821
  findings.push({
10663
10822
  pkg: p,
10664
10823
  version,
@@ -10668,7 +10827,7 @@ async function checkOne(p, cfg, misses, networkCapped) {
10668
10827
  }
10669
10828
  return findings;
10670
10829
  }
10671
- async function runPackageCheck(toolName, args, cfg) {
10830
+ async function runPackageCheck(toolName, args, cfg, cwd) {
10672
10831
  if (!cfg.enabled) return ALLOW;
10673
10832
  try {
10674
10833
  const command = commandOf(toolName, args);
@@ -10679,7 +10838,7 @@ async function runPackageCheck(toolName, args, cfg) {
10679
10838
  const misses = [];
10680
10839
  const checked = requests;
10681
10840
  const settled = await Promise.allSettled(
10682
- checked.map((p, i) => checkOne(p, cfg, misses, i >= MAX_NETWORK_PACKAGES))
10841
+ checked.map((p, i) => checkOne(p, cfg, misses, i >= MAX_NETWORK_PACKAGES, cwd))
10683
10842
  );
10684
10843
  const findings = [];
10685
10844
  settled.forEach((s, i) => {
@@ -11087,7 +11246,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
11087
11246
  }
11088
11247
  const packageCheck = config.policy.packageCheck;
11089
11248
  if (packageCheck?.enabled && !isManual) {
11090
- const pkg = await runPackageCheck(toolName, args, packageCheck);
11249
+ const pkg = await runPackageCheck(toolName, args, packageCheck, options?.cwd);
11091
11250
  if (pkg.misses.length > 0) {
11092
11251
  appendToLog(HOOK_DEBUG_LOG, {
11093
11252
  ts: (/* @__PURE__ */ new Date()).toISOString(),
package/dist/scan-ink.mjs CHANGED
@@ -196,7 +196,10 @@ var ConfigFileSchema = z.object({
196
196
  packageCheck: z.object({
197
197
  enabled: z.boolean().optional(),
198
198
  onMalicious: z.enum(["block", "review"]).optional(),
199
+ // 2.27.0 shape, still accepted: false = newPackage and installScript off.
199
200
  registrySignals: z.boolean().optional(),
201
+ newPackage: z.enum(["review", "off"]).optional(),
202
+ installScript: z.enum(["review", "off"]).optional(),
200
203
  maxAgeHours: z.number().min(0).optional(),
201
204
  onlineFallback: z.boolean().optional(),
202
205
  allow: z.array(z.string()).optional()
@@ -2800,14 +2803,19 @@ var TUNING_FIELDS = [
2800
2803
  { checkId: "loading.skill-tamper", name: "roots", legacy: ["skillPinning", "roots"] },
2801
2804
  {
2802
2805
  checkId: "loading.malicious-package",
2803
- name: "registrySignals",
2804
- legacy: ["packageCheck", "registrySignals"]
2806
+ name: "newPackage",
2807
+ legacy: ["packageCheck", "newPackage"]
2805
2808
  },
2806
2809
  {
2807
2810
  checkId: "loading.malicious-package",
2808
2811
  name: "maxAgeHours",
2809
2812
  legacy: ["packageCheck", "maxAgeHours"]
2810
2813
  },
2814
+ {
2815
+ checkId: "loading.malicious-package",
2816
+ name: "installScript",
2817
+ legacy: ["packageCheck", "installScript"]
2818
+ },
2811
2819
  {
2812
2820
  checkId: "loading.malicious-package",
2813
2821
  name: "onlineFallback",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.27.1",
3
+ "version": "2.28.0",
4
4
  "description": "Access control for AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -99,6 +99,7 @@
99
99
  "picomatch": "^4.0.3",
100
100
  "react": "^19.2.6",
101
101
  "safe-regex2": "^5.1.0",
102
+ "semver": "^7.8.5",
102
103
  "smol-toml": "^1.6.1",
103
104
  "string-width": "^8.3.0",
104
105
  "yaml": "^2.9.0",
@@ -118,6 +119,7 @@
118
119
  "@types/node": "^26.4.1",
119
120
  "@types/picomatch": "^4.0.2",
120
121
  "@types/react": "^19.2.14",
122
+ "@types/semver": "^7.8.0",
121
123
  "@vitest/coverage-v8": "^5.0.0",
122
124
  "cross-env": "^10.1.0",
123
125
  "ink-testing-library": "^4.0.0",