pmsec 2026.11.3 → 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 +1 -1
- package/src/cli.mjs +8 -3
- package/src/tools/composer.mjs +38 -24
- package/src/tools/npm.mjs +5 -3
- package/src/tools/uv.mjs +4 -2
- package/src/util/io.mjs +20 -45
- package/src/util/paths.mjs +21 -5
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -188,8 +188,10 @@ function runDoctor(targets, json, ctx, out) {
|
|
|
188
188
|
platform: ctx.platform,
|
|
189
189
|
uid, euid, username,
|
|
190
190
|
home: ctx.home,
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
// This port resolves paths from HOME only; PMSEC_HOME is a bash/powershell
|
|
192
|
+
// feature (it needs the root->owner chown handback those ports implement).
|
|
193
|
+
pmsecHome: null,
|
|
194
|
+
pmsecHomeSource: "HOME",
|
|
193
195
|
tools,
|
|
194
196
|
ok: okFlag,
|
|
195
197
|
};
|
|
@@ -201,7 +203,7 @@ function runDoctor(targets, json, ctx, out) {
|
|
|
201
203
|
out.write(` platform : ${ctx.platform}\n`);
|
|
202
204
|
if (isPosix) out.write(` uid/euid : ${uid}/${euid}${username ? ` (${username})` : ""}\n`);
|
|
203
205
|
out.write(` HOME : ${ctx.home}\n`);
|
|
204
|
-
out.write(` PMSEC_HOME :
|
|
206
|
+
out.write(` PMSEC_HOME : (not supported by this port — using HOME)\n\n`);
|
|
205
207
|
for (const t of tools) {
|
|
206
208
|
const flag = t.parentWritable ? "ok " : "BLOCK ";
|
|
207
209
|
const ownerSuffix = t.exists && t.owner !== null ? ` (uid=${t.owner})` : "";
|
|
@@ -299,6 +301,9 @@ export async function run(argv, {
|
|
|
299
301
|
try { targets = selectTools(opts.only); }
|
|
300
302
|
catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
|
|
301
303
|
const ctx = { env, home, platform };
|
|
304
|
+
if (env.PMSEC_HOME) {
|
|
305
|
+
err.write(`pmsec: PMSEC_HOME is ignored by the node port; paths resolve from HOME (${home}). Use the bash port for root-context redirection.\n`);
|
|
306
|
+
}
|
|
302
307
|
try {
|
|
303
308
|
if (opts.mode === "check") return await runCheck(targets, opts, ctx, out, err);
|
|
304
309
|
if (opts.mode === "disable") return await runDisable(targets, opts.json, ctx, out, err);
|
package/src/tools/composer.mjs
CHANGED
|
@@ -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
|
-
|
|
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#
|
|
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
|
-
//
|
|
17
|
-
|
|
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*(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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: "
|
|
14
|
-
{ key: "allow-remote", expected: "
|
|
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" },
|
|
@@ -29,9 +29,11 @@ export async function read(ctx) {
|
|
|
29
29
|
const p = path(ctx);
|
|
30
30
|
const raw = await readSafe(p);
|
|
31
31
|
const value = readKey(raw, key);
|
|
32
|
+
// npm stores whole days. Anything else is reported as unparseable (null) so
|
|
33
|
+
// `--check` fails instead of letting NaN slip past the `< days` comparison.
|
|
32
34
|
return {
|
|
33
35
|
path: p, configured: value,
|
|
34
|
-
days: value
|
|
36
|
+
days: value !== null && /^\d+$/.test(value) ? Number(value) : null,
|
|
35
37
|
extras: readExtras(raw, extras)
|
|
36
38
|
};
|
|
37
39
|
}
|
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
|
-
|
|
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
|
-
"
|
|
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;
|
package/src/util/io.mjs
CHANGED
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
import { mkdir, readFile,
|
|
2
|
-
import { dirname
|
|
1
|
+
import { mkdir, readFile, rename, lstat, unlink, open } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
5
|
|
|
8
6
|
export async function readSafe(path) {
|
|
9
7
|
// Editors on Windows may prepend a BOM; it would hide the first header or key.
|
|
10
|
-
try { return (await readFile(path, "utf8")).replace(
|
|
8
|
+
try { return (await readFile(path, "utf8")).replace(/^/, ""); }
|
|
11
9
|
catch (e) { if (e.code === "ENOENT") return ""; throw e; }
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
function isPermErr(e) { return e?.code === "EACCES" || e?.code === "EPERM"; }
|
|
15
|
-
|
|
16
12
|
async function isSymlink(p) {
|
|
17
13
|
try { return (await lstat(p)).isSymbolicLink(); }
|
|
18
14
|
catch { return false; }
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
function underHome(p, home) {
|
|
22
|
-
const r = resolve(p);
|
|
23
|
-
const h = resolve(home);
|
|
24
|
-
return r === h || r.startsWith(h + sep);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
export function shellQuote(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
if (!underHome(path, home)) {
|
|
37
|
-
err.write(`pmsec: refusing to chown ${path} outside HOME (${home}); fix ownership manually.\n`);
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
err.write(`pmsec: ${path} not writable; running \`sudo chown -h $(id -u):$(id -g) ${shellQuote(path)}\`. You may be prompted for your password.\n`);
|
|
41
|
-
const r = spawnSync("sudo", ["chown", "-h", `${process.getuid()}:${process.getgid()}`, path], { stdio: "inherit" });
|
|
42
|
-
return r.status === 0;
|
|
19
|
+
function symlinkError(what, p) {
|
|
20
|
+
const e = new Error(`refusing to write ${what} through symlink ${p}`);
|
|
21
|
+
e.code = "ELOOP";
|
|
22
|
+
e.path = p;
|
|
23
|
+
return e;
|
|
43
24
|
}
|
|
44
25
|
|
|
45
26
|
async function atomicReplace(path, text) {
|
|
@@ -59,24 +40,18 @@ async function atomicReplace(path, text) {
|
|
|
59
40
|
}
|
|
60
41
|
}
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
// Permission errors propagate to the caller, which prints a chown hint. pmsec
|
|
44
|
+
// never escalates privileges itself: an unwritable config under HOME is an
|
|
45
|
+
// operator decision (or a symptom worth seeing), not something to sudo past.
|
|
46
|
+
export async function writeAtomic(path, text, { backup = true } = {}) {
|
|
63
47
|
await mkdir(dirname(path), { recursive: true });
|
|
64
|
-
if (existsSync(path) && await isSymlink(path))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
catch (e) {
|
|
72
|
-
const target = existsSync(path + ".bak") ? path + ".bak" : path;
|
|
73
|
-
if (!isPermErr(e) || !(await reclaim(target, home, err))) throw e;
|
|
74
|
-
await atomicReplace(path + ".bak", await readFile(path, "utf8"));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
try { await atomicReplace(path, text); }
|
|
78
|
-
catch (e) {
|
|
79
|
-
if (!isPermErr(e) || !(await reclaim(path, home, err))) throw e;
|
|
80
|
-
await atomicReplace(path, text);
|
|
48
|
+
if (existsSync(path) && await isSymlink(path)) throw symlinkError("config", path);
|
|
49
|
+
const bak = path + ".bak";
|
|
50
|
+
if (backup && existsSync(path) && !existsSync(bak)) {
|
|
51
|
+
// A dangling symlink fails existsSync; rename() would replace the link
|
|
52
|
+
// itself, but refuse anyway so a planted link is visible, not silently clobbered.
|
|
53
|
+
if (await isSymlink(bak)) throw symlinkError("backup", bak);
|
|
54
|
+
await atomicReplace(bak, await readFile(path, "utf8"));
|
|
81
55
|
}
|
|
56
|
+
await atomicReplace(path, text);
|
|
82
57
|
}
|
package/src/util/paths.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
}
|