pmsec 2026.11.4 → 2026.11.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "2026.11.4",
3
+ "version": "2026.11.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,11 +1,18 @@
1
1
  import { composerConfigPath } from "../util/paths.mjs";
2
2
  import { readSafe, writeAtomic } from "../util/io.mjs";
3
3
  import { readJsonKey, setJsonKey, removeJsonKey } from "../util/jsonconf.mjs";
4
+ import { buildPreflight } from "../util/version.mjs";
4
5
 
5
6
  export const name = "composer";
6
- export const key = "minimum-release-age";
7
+ // Composer's cooldown lives under the unified policy block (Composer >= 2.11):
8
+ // { "config": { "policy": { "cooldown": { "period": "7 days" } } } }
9
+ // `config.minimum-release-age` was never a Composer key; earlier pmsec
10
+ // releases wrote it and Composer silently ignored it. It is removed on write.
11
+ export const key = "policy.cooldown.period";
7
12
  export const section = "config";
8
- export const docs = "https://getcomposer.org/doc/06-config.md#minimum-release-age";
13
+ export const docs = "https://getcomposer.org/doc/06-config.md#policy";
14
+ export const minBin = [2, 11, 0];
15
+ const COOLDOWN_PATH = ["config", "policy", "cooldown", "period"];
9
16
  export const extras = [
10
17
  { key: "allow-plugins", expected: "false", line: "false", path: ["config", "allow-plugins"] },
11
18
  { key: "secure-http", expected: "true", line: "true", path: ["config", "secure-http"] },
@@ -13,37 +20,34 @@ export const extras = [
13
20
  { key: "abandoned", expected: '"fail"', line: '"fail"', path: ["config", "audit", "abandoned"] },
14
21
  ];
15
22
 
16
- // Composer is newer than most tools in the bundle; accept the shorter npm
17
- // spelling too so a config written by hand is still recognized.
18
- const PRIMARY_ALIASES = [key, "min-release-age", "minimum_release_age", "minimumReleaseAge"];
23
+ // Keys written by pmsec < 2026.11.5 that Composer never read.
24
+ const LEGACY_KEYS = ["minimum-release-age", "min-release-age", "minimum_release_age", "minimumReleaseAge"];
19
25
 
20
26
  export function path(ctx) { return composerConfigPath(ctx.env, ctx.home, ctx.platform); }
21
27
 
28
+ export const preflight = buildPreflight(name, minBin,
29
+ "config.policy.cooldown is silently ignored. Upgrade Composer to enforce the cooldown.");
30
+
31
+ // Composer: "<n> second|minute|hour|day|week[s]", or an integer number of
32
+ // seconds (a bare numeric string counts as seconds too). Partial days floor.
22
33
  function parseDays(value) {
23
34
  if (value === null) return null;
24
35
  const v = String(value).trim().replace(/^"|"$/g, "").trim();
25
- const dur = v.match(/^(\d+)\s*(d|days?|w|weeks?)$/i);
36
+ const dur = v.match(/^(\d+)\s*(seconds?|minutes?|hours?|days?|weeks?)$/i);
26
37
  if (dur) {
27
38
  const n = Number(dur[1]);
28
- return /^(w|weeks?)$/i.test(dur[2]) ? n * 7 : n;
39
+ const unit = dur[2].toLowerCase().replace(/s$/, "");
40
+ const secs = { second: 1, minute: 60, hour: 3600, day: 86400, week: 604800 }[unit];
41
+ return Math.floor((n * secs) / 86400);
29
42
  }
30
43
  if (!/^\d+$/.test(v)) return null;
31
- const n = Number(v);
32
- // Large values are seconds or minutes (the units Composer uses for its
33
- // other time settings); small values are days.
34
- if (n >= 86400) return Math.floor(n / 86400);
35
- if (n >= 1440 && n % 1440 === 0) return n / 1440;
36
- return n;
44
+ return Math.floor(Number(v) / 86400);
37
45
  }
38
46
 
39
47
  export async function read(ctx) {
40
48
  const p = path(ctx);
41
49
  const raw = await readSafe(p);
42
- let configured = null;
43
- for (const candidate of PRIMARY_ALIASES) {
44
- const v = readJsonKey(raw, ["config", candidate]);
45
- if (v !== null) { configured = v; break; }
46
- }
50
+ const configured = readJsonKey(raw, COOLDOWN_PATH);
47
51
  return {
48
52
  path: p, configured, days: parseDays(configured),
49
53
  extras: extras.map(e => {
@@ -54,10 +58,21 @@ export async function read(ctx) {
54
58
  };
55
59
  }
56
60
 
61
+ function dropLegacy(text) {
62
+ let removed = false;
63
+ for (const k of LEGACY_KEYS) {
64
+ const r = removeJsonKey(text, ["config", k]);
65
+ text = r.text;
66
+ if (r.removed) removed = true;
67
+ }
68
+ return { text, removed };
69
+ }
70
+
57
71
  export async function write(days, ctx) {
58
72
  const p = path(ctx);
59
73
  let text = await readSafe(p);
60
- text = setJsonKey(text, ["config", key], `${days}`);
74
+ text = dropLegacy(text).text;
75
+ text = setJsonKey(text, COOLDOWN_PATH, `"${days} days"`);
61
76
  for (const e of extras) text = setJsonKey(text, e.path, e.line);
62
77
  await writeAtomic(p, text);
63
78
  return { path: p };
@@ -67,11 +82,10 @@ export async function unset(ctx) {
67
82
  const p = path(ctx);
68
83
  let text = await readSafe(p);
69
84
  let removed = false;
70
- for (const candidate of PRIMARY_ALIASES) {
71
- const r = removeJsonKey(text, ["config", candidate]);
72
- text = r.text;
73
- if (r.removed) removed = true;
74
- }
85
+ const r0 = removeJsonKey(text, COOLDOWN_PATH);
86
+ text = r0.text; removed = r0.removed;
87
+ const r1 = dropLegacy(text);
88
+ text = r1.text; removed = removed || r1.removed;
75
89
  for (const e of extras) {
76
90
  const r = removeJsonKey(text, e.path);
77
91
  text = r.text;
package/src/tools/npm.mjs CHANGED
@@ -10,8 +10,8 @@ export const docs = "https://docs.npmjs.com/cli/v11/using-npm/config#min-release
10
10
  export const minBin = [11, 10, 0];
11
11
  export const extras = [
12
12
  { key: "audit-level", expected: "high", line: "audit-level=high" },
13
- { key: "allow-git", expected: "root", line: "allow-git=root", safeValues: ["none", "root"] },
14
- { key: "allow-remote", expected: "root", line: "allow-remote=root", safeValues: ["none", "root"] },
13
+ { key: "allow-git", expected: "none", line: "allow-git=none" },
14
+ { key: "allow-remote", expected: "none", line: "allow-remote=none" },
15
15
  { key: "allow-file", expected: "root", line: "allow-file=root", safeValues: ["none", "root"] },
16
16
  { key: "allow-directory", expected: "root", line: "allow-directory=root", safeValues: ["none", "root"] },
17
17
  { key: "strict-allow-scripts", expected: "true", line: "strict-allow-scripts=true" },
package/src/tools/uv.mjs CHANGED
@@ -7,7 +7,9 @@ import { buildPreflight } from "../util/version.mjs";
7
7
  export const name = "uv";
8
8
  export const key = "exclude-newer";
9
9
  export const docs = "https://docs.astral.sh/uv/reference/settings/#exclude-newer";
10
- export const minBin = [0, 9, 17];
10
+ // 0.9.17 added relative exclude-newer; the [audit] table only parses from 0.11.3
11
+ // (verified against released binaries: 0.11.2 rejects it, 0.11.3 accepts it).
12
+ export const minBin = [0, 11, 3];
11
13
  export const extras = [
12
14
  { key: "index-strategy", expected: '"first-index"', line: 'index-strategy = "first-index"' },
13
15
  { key: "malware-check", expected: "true", line: "malware-check = true", section: "audit" },
@@ -16,7 +18,7 @@ export const extras = [
16
18
  export function path(ctx) { return uvConfigPath(ctx.env, ctx.home, ctx.platform); }
17
19
 
18
20
  export const preflight = buildPreflight(name, minBin,
19
- "writing exclude-newer = \"N days\" will break this uv until you `uv self update` (file will fail to parse).");
21
+ "the written uv.toml (relative exclude-newer and the [audit] table) will not parse on this uv and every uv command will fail until you `uv self update`.");
20
22
 
21
23
  function parseDays(value) {
22
24
  if (value === null) return null;
@@ -1,5 +1,6 @@
1
1
  import { homedir } from "node:os";
2
- import { dirname, join } from "node:path";
2
+ import { dirname, join, isAbsolute } from "node:path";
3
+ import { existsSync } from "node:fs";
3
4
 
4
5
  export function npmrcPath(env = process.env, home = homedir()) {
5
6
  return env.NPM_CONFIG_USERCONFIG || join(home, ".npmrc");
@@ -37,8 +38,12 @@ export function uvConfigPath(env = process.env, home = homedir(), platform = pro
37
38
  return join(base, "uv", "uv.toml");
38
39
  }
39
40
 
41
+ // bun reads $XDG_CONFIG_HOME/.bunfig.toml *instead of* $HOME/.bunfig.toml
42
+ // whenever XDG_CONFIG_HOME is set to an absolute path.
40
43
  export function bunConfigPath(env = process.env, home = homedir()) {
41
- return env.BUN_CONFIG_FILE || join(home, ".bunfig.toml");
44
+ if (env.BUN_CONFIG_FILE) return env.BUN_CONFIG_FILE;
45
+ if (env.XDG_CONFIG_HOME && isAbsolute(env.XDG_CONFIG_HOME)) return join(env.XDG_CONFIG_HOME, ".bunfig.toml");
46
+ return join(home, ".bunfig.toml");
42
47
  }
43
48
 
44
49
  export function yarnrcPath(env = process.env, home = homedir()) {
@@ -83,13 +88,24 @@ export function aubeConfigPath(env = process.env, home = homedir(), platform = p
83
88
  // COMPOSER_HOME follows platform conventions (XDG on Unix, Application Support
84
89
  // on macOS, APPDATA on Windows), matching Composer's own resolution. An
85
90
  // explicit XDG_CONFIG_HOME wins on every non-Windows platform, including macOS.
91
+ // Mirrors Composer's Factory::getHomeDir: COMPOSER_HOME, else %APPDATA%/Composer
92
+ // on Windows, else (when any XDG_* variable is set or /etc/xdg exists) the
93
+ // first existing of $XDG_CONFIG_HOME/composer and ~/.composer, defaulting to
94
+ // the XDG one; without XDG, ~/.composer. Composer never uses Application
95
+ // Support on macOS.
86
96
  export function composerConfigPath(env = process.env, home = homedir(), platform = process.platform) {
87
97
  if (env.COMPOSER_HOME) return join(env.COMPOSER_HOME, "config.json");
88
98
  if (platform === "win32") {
89
99
  const base = env.APPDATA || join(home, "AppData", "Roaming");
90
100
  return join(base, "Composer", "config.json");
91
101
  }
92
- if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "composer", "config.json");
93
- if (platform === "darwin") return join(home, "Library", "Application Support", "composer", "config.json");
94
- return join(home, ".config", "composer", "config.json");
102
+ // The /etc/xdg probe describes the host, so it only applies when the
103
+ // requested platform is the host platform (tests pass foreign platforms).
104
+ const useXdg = Object.keys(env).some(k => k.startsWith("XDG_"))
105
+ || (platform === process.platform && existsSync("/etc/xdg"));
106
+ const dirs = [];
107
+ if (useXdg) dirs.push(join(env.XDG_CONFIG_HOME || join(home, ".config"), "composer"));
108
+ dirs.push(join(home, ".composer"));
109
+ const dir = dirs.find(d => existsSync(d)) ?? dirs[0];
110
+ return join(dir, "config.json");
95
111
  }