@homeflare/alchemy 0.25.1 → 0.26.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.
Files changed (60) hide show
  1. package/dist/launchd/index.js +51 -47
  2. package/dist/launchd/index.js.map +5 -4
  3. package/dist/launchd/sudo-guard-mode.d.ts +62 -0
  4. package/dist/launchd/sudo-guard-mode.d.ts.map +1 -0
  5. package/dist/launchd/sudo-guard.d.ts +1 -38
  6. package/dist/launchd/sudo-guard.d.ts.map +1 -1
  7. package/dist/linux/fake-sudo-root.d.ts +26 -0
  8. package/dist/linux/fake-sudo-root.d.ts.map +1 -0
  9. package/dist/linux/fake-sudo.d.ts +56 -0
  10. package/dist/linux/fake-sudo.d.ts.map +1 -0
  11. package/dist/linux/index.d.ts +2 -0
  12. package/dist/linux/index.d.ts.map +1 -1
  13. package/dist/linux/index.js +1134 -2
  14. package/dist/linux/index.js.map +22 -5
  15. package/dist/linux/sudo-allowlist-chown.d.ts +12 -0
  16. package/dist/linux/sudo-allowlist-chown.d.ts.map +1 -0
  17. package/dist/linux/sudo-allowlist-dir.d.ts +6 -0
  18. package/dist/linux/sudo-allowlist-dir.d.ts.map +1 -0
  19. package/dist/linux/sudo-allowlist.d.ts +40 -0
  20. package/dist/linux/sudo-allowlist.d.ts.map +1 -0
  21. package/dist/linux/sudo-exec.d.ts +31 -0
  22. package/dist/linux/sudo-exec.d.ts.map +1 -0
  23. package/dist/linux/sudo-guard.d.ts +36 -0
  24. package/dist/linux/sudo-guard.d.ts.map +1 -0
  25. package/dist/linux/sudo-runner.d.ts +37 -0
  26. package/dist/linux/sudo-runner.d.ts.map +1 -0
  27. package/dist/linux/sudo-stage.d.ts +22 -0
  28. package/dist/linux/sudo-stage.d.ts.map +1 -0
  29. package/dist/linux/sudo-write.d.ts +25 -0
  30. package/dist/linux/sudo-write.d.ts.map +1 -0
  31. package/dist/linux/unit-form.d.ts +2 -0
  32. package/dist/linux/unit-form.d.ts.map +1 -1
  33. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  34. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
  35. package/docs/launchd-sudo.md +12 -3
  36. package/docs/linux-host.md +3 -3
  37. package/docs/linux-sudo.md +187 -0
  38. package/package.json +1 -1
  39. package/src/launchd/sudo-guard-mode.ts +111 -0
  40. package/src/launchd/sudo-guard.ts +3 -79
  41. package/src/launchd/sudo-modes.test.ts +38 -1
  42. package/src/linux/fake-sudo-root.ts +157 -0
  43. package/src/linux/fake-sudo.ts +142 -0
  44. package/src/linux/index.ts +2 -0
  45. package/src/linux/sudo-allowlist-chown.ts +71 -0
  46. package/src/linux/sudo-allowlist-dir.ts +103 -0
  47. package/src/linux/sudo-allowlist.test.ts +252 -0
  48. package/src/linux/sudo-allowlist.ts +223 -0
  49. package/src/linux/sudo-exec.ts +118 -0
  50. package/src/linux/sudo-guard.test.ts +136 -0
  51. package/src/linux/sudo-guard.ts +142 -0
  52. package/src/linux/sudo-lifecycle.test.ts +151 -0
  53. package/src/linux/sudo-runner.test.ts +206 -0
  54. package/src/linux/sudo-runner.ts +164 -0
  55. package/src/linux/sudo-stage.ts +50 -0
  56. package/src/linux/sudo-write.ts +167 -0
  57. package/src/linux/unit-form.ts +2 -1
  58. package/src/litellm/fake-litellm.ts +2 -0
  59. package/src/proxmox/client.test.ts +17 -3
  60. package/src/proxmox/fake-pve-lxc.ts +10 -1
@@ -1590,6 +1590,1137 @@ var sshRunner = async (options) => {
1590
1590
  }
1591
1591
  };
1592
1592
  };
1593
+ // src/launchd/local-runner.ts
1594
+ import { spawn as spawn2 } from "node:child_process";
1595
+ import { randomBytes as randomBytes2 } from "node:crypto";
1596
+ import { lstat, open, readFile, rename, unlink } from "node:fs/promises";
1597
+ import { basename, dirname, join } from "node:path";
1598
+ var errnoCode = (cause) => typeof cause === "object" && cause !== null && ("code" in cause) ? String(cause.code) : undefined;
1599
+ var absent2 = (cause) => errnoCode(cause) === "ENOENT" || errnoCode(cause) === "ENOTDIR";
1600
+ var run2 = (argv, timeoutMs) => new Promise((resolve, reject) => {
1601
+ const [program, ...args] = argv;
1602
+ if (program === undefined) {
1603
+ reject(new Error("exec: empty argv"));
1604
+ return;
1605
+ }
1606
+ const child = spawn2(program, args, {
1607
+ shell: false,
1608
+ stdio: ["ignore", "pipe", "pipe"],
1609
+ timeout: timeoutMs
1610
+ });
1611
+ const out = [];
1612
+ const err = [];
1613
+ child.stdout.on("data", (chunk) => out.push(chunk));
1614
+ child.stderr.on("data", (chunk) => err.push(chunk));
1615
+ child.on("error", (cause) => {
1616
+ reject(new Error(`exec ${program}: ${cause.message}`));
1617
+ });
1618
+ child.on("close", (exitCode, signal) => {
1619
+ const stderr = Buffer.concat(err).toString("utf8");
1620
+ resolve({
1621
+ exitCode: exitCode ?? -1,
1622
+ stderr: signal === null ? stderr : `${stderr}
1623
+ (killed by ${signal})`.trim(),
1624
+ stdout: Buffer.concat(out).toString("utf8")
1625
+ });
1626
+ });
1627
+ });
1628
+ var kindOf = (stats) => {
1629
+ if (stats.isSymbolicLink())
1630
+ return "symlink";
1631
+ if (stats.isFile())
1632
+ return "file";
1633
+ if (stats.isDirectory())
1634
+ return "directory";
1635
+ return "other";
1636
+ };
1637
+ var writeAtomic = async (path, bytes, options) => {
1638
+ const temp = join(dirname(path), `.${basename(path)}.${randomBytes2(6).toString("hex")}.tmp`);
1639
+ const handle = await open(temp, "wx", 384);
1640
+ try {
1641
+ try {
1642
+ await handle.writeFile(bytes);
1643
+ if (options.uid !== undefined || options.gid !== undefined) {
1644
+ await handle.chown(options.uid ?? -1, options.gid ?? -1);
1645
+ }
1646
+ await handle.chmod(options.mode);
1647
+ await handle.sync();
1648
+ } finally {
1649
+ await handle.close();
1650
+ }
1651
+ await rename(temp, path);
1652
+ } catch (cause) {
1653
+ await unlink(temp).catch(() => {
1654
+ return;
1655
+ });
1656
+ throw cause;
1657
+ }
1658
+ };
1659
+ var localRunner = (options = {}) => {
1660
+ const timeoutMs = options.execTimeoutMs ?? 120000;
1661
+ const platform = process.platform;
1662
+ return {
1663
+ effectiveUid: () => process.geteuid?.() ?? -1,
1664
+ exec: (argv) => run2(argv, timeoutMs),
1665
+ lookupGroup: async (nameOrId) => {
1666
+ const result = await run2(groupQuery(platform, nameOrId), timeoutMs);
1667
+ return result.exitCode === 0 ? parseGroup(platform, result.stdout) : undefined;
1668
+ },
1669
+ lookupUser: async (nameOrId) => {
1670
+ const result = await run2(userQuery(platform, nameOrId), timeoutMs);
1671
+ return result.exitCode === 0 ? parseUser(platform, result.stdout) : undefined;
1672
+ },
1673
+ privileged: false,
1674
+ readFile: async (path) => {
1675
+ try {
1676
+ return new Uint8Array(await readFile(path));
1677
+ } catch (cause) {
1678
+ if (absent2(cause))
1679
+ return;
1680
+ throw cause;
1681
+ }
1682
+ },
1683
+ removeFile: async (path) => {
1684
+ try {
1685
+ await unlink(path);
1686
+ } catch (cause) {
1687
+ if (!absent2(cause))
1688
+ throw cause;
1689
+ }
1690
+ },
1691
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1692
+ stat: async (path) => {
1693
+ try {
1694
+ const stats = await lstat(path);
1695
+ return {
1696
+ dev: stats.dev,
1697
+ gid: stats.gid,
1698
+ ino: stats.ino,
1699
+ kind: kindOf(stats),
1700
+ mode: stats.mode & 4095,
1701
+ size: stats.size,
1702
+ uid: stats.uid
1703
+ };
1704
+ } catch (cause) {
1705
+ if (absent2(cause))
1706
+ return;
1707
+ throw cause;
1708
+ }
1709
+ },
1710
+ writeFileAtomic: writeAtomic
1711
+ };
1712
+ };
1713
+
1714
+ // src/launchd/job-validate.ts
1715
+ var LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
1716
+ var RESERVED_LABEL_PREFIXES = [
1717
+ "org.nixos.",
1718
+ "com.apple.",
1719
+ "homebrew.mxcl."
1720
+ ];
1721
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1722
+ var RANGES = [
1723
+ ["minute", 0, 59],
1724
+ ["hour", 0, 23],
1725
+ ["day", 1, 31],
1726
+ ["weekday", 0, 7],
1727
+ ["month", 1, 12]
1728
+ ];
1729
+ var isInt = (value, min, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(value) && value >= min && value <= max;
1730
+ var calendarProblems = (entries) => {
1731
+ if (entries.length === 0)
1732
+ return ["startCalendarInterval is an empty list; omit it instead"];
1733
+ const found = [];
1734
+ for (const [index, entry] of entries.entries()) {
1735
+ for (const [field, min, max] of RANGES) {
1736
+ const value = entry[field];
1737
+ if (value !== undefined && !isInt(value, min, max)) {
1738
+ found.push(`startCalendarInterval[${String(index)}].${field} must be ${String(min)}–${String(max)}`);
1739
+ }
1740
+ }
1741
+ }
1742
+ return found;
1743
+ };
1744
+ var absolute = (name, value) => value === undefined || value.startsWith("/") ? [] : [`${name} must be an absolute path`];
1745
+ var identityProblems3 = (label, domainText) => {
1746
+ const found = [];
1747
+ if (!LABEL.test(label)) {
1748
+ found.push("label must be 1–200 of [A-Za-z0-9._-], starting with a letter or digit");
1749
+ }
1750
+ const reserved = RESERVED_LABEL_PREFIXES.find((prefix) => label.startsWith(prefix));
1751
+ if (reserved !== undefined) {
1752
+ found.push(`label ${label} is under ${reserved}, which another tool owns; declare a new label`);
1753
+ }
1754
+ if (parseDomain(domainText) === undefined)
1755
+ found.push(`domain must be 'system' or 'gui/<uid>', not ${domainText}`);
1756
+ return found;
1757
+ };
1758
+ var jobProblems = (props) => {
1759
+ const found = identityProblems3(props.label, props.domain);
1760
+ const domain = parseDomain(props.domain);
1761
+ const [program] = props.programArguments;
1762
+ if (program === undefined)
1763
+ found.push("programArguments must name a program");
1764
+ else if (!program.startsWith("/"))
1765
+ found.push("programArguments[0] must be an absolute path");
1766
+ for (const name of Object.keys(props.environment ?? {})) {
1767
+ if (!ENV_NAME.test(name))
1768
+ found.push(`environment name ${JSON.stringify(name)} is not a valid variable name`);
1769
+ }
1770
+ found.push(...jobSecretProblems(props.environment, props.programArguments, props.extraKeys));
1771
+ if (props.startInterval !== undefined && !isInt(props.startInterval, 1)) {
1772
+ found.push("startInterval must be a positive whole number of seconds");
1773
+ }
1774
+ if (props.throttleInterval !== undefined && !isInt(props.throttleInterval, 0)) {
1775
+ found.push("throttleInterval must be a non-negative whole number of seconds");
1776
+ }
1777
+ const calendar = props.startCalendarInterval;
1778
+ if (calendar !== undefined) {
1779
+ found.push(...calendarProblems(Array.isArray(calendar) ? calendar : [calendar]));
1780
+ }
1781
+ const keepAlive = props.keepAlive;
1782
+ if (typeof keepAlive === "object" && keepAlive.successfulExit === undefined && keepAlive.crashed === undefined) {
1783
+ found.push("keepAlive {} names no condition; use true, false, or a condition");
1784
+ }
1785
+ found.push(...absolute("standardOutPath", props.standardOutPath));
1786
+ found.push(...absolute("standardErrorPath", props.standardErrorPath));
1787
+ found.push(...absolute("workingDirectory", props.workingDirectory));
1788
+ if (domain?.kind === "gui" && (props.userName !== undefined || props.groupName !== undefined)) {
1789
+ found.push("userName/groupName apply only to the system domain; launchd ignores them for agents");
1790
+ }
1791
+ for (const key of Object.keys(props.extraKeys ?? {})) {
1792
+ if (OWNED_KEYS.includes(key))
1793
+ found.push(`extraKeys.${key} is owned by a typed prop; use that instead`);
1794
+ }
1795
+ return found;
1796
+ };
1797
+
1798
+ // src/launchd/launchctl.ts
1799
+ var LAUNCHCTL = "/bin/launchctl";
1800
+ var NOT_FOUND = 113;
1801
+ var NO_DOMAIN = 112;
1802
+ var NO_DOMAIN_TEXT = "Could not find domain";
1803
+
1804
+ class LaunchctlError extends Error {
1805
+ constructor(command, exitCode, stderr) {
1806
+ super(`launchctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
1807
+ this.name = "LaunchctlError";
1808
+ }
1809
+ }
1810
+ var printFields = (stdout) => {
1811
+ const fields = new Map;
1812
+ const closers = [];
1813
+ for (const line of stdout.split(`
1814
+ `)) {
1815
+ if (closers.length === 0) {
1816
+ if (fields.size === 0 && line.endsWith(" = {"))
1817
+ closers.push("}");
1818
+ continue;
1819
+ }
1820
+ if (line === closers[closers.length - 1]) {
1821
+ closers.pop();
1822
+ continue;
1823
+ }
1824
+ const opened = /^(\t+)\S.* = \{$/.exec(line);
1825
+ if (opened?.[1] !== undefined && opened[1].length === closers.length) {
1826
+ closers.push(`${opened[1]}}`);
1827
+ continue;
1828
+ }
1829
+ const field = closers.length === 1 ? /^\t([a-z][a-z ]*[a-z]) = (.*)$/.exec(line) : null;
1830
+ if (field?.[1] !== undefined && field[2] !== undefined && !fields.has(field[1])) {
1831
+ fields.set(field[1], field[2]);
1832
+ }
1833
+ }
1834
+ return fields;
1835
+ };
1836
+ var leadingInt = (text) => {
1837
+ const match = text === undefined ? null : /^(-?\d+)/.exec(text);
1838
+ return match?.[1] === undefined ? undefined : Number(match[1]);
1839
+ };
1840
+ var parsePrint = (stdout) => {
1841
+ const fields = printFields(stdout);
1842
+ const state = fields.get("state");
1843
+ const pid = leadingInt(fields.get("pid"));
1844
+ const lastExitCode = leadingInt(fields.get("last exit code"));
1845
+ return {
1846
+ loaded: true,
1847
+ ...state === undefined ? {} : { state },
1848
+ ...pid === undefined ? {} : { pid },
1849
+ ...lastExitCode === undefined ? {} : { lastExitCode }
1850
+ };
1851
+ };
1852
+ var parseDisabled = (stdout) => {
1853
+ const disabled = new Set;
1854
+ for (const match of stdout.matchAll(/^\s*"([^"]+)" => (\w+)\s*$/gm)) {
1855
+ if (match[1] !== undefined && (match[2] === "disabled" || match[2] === "true")) {
1856
+ disabled.add(match[1]);
1857
+ }
1858
+ }
1859
+ return disabled;
1860
+ };
1861
+ var printService = async (runner, target) => {
1862
+ const result = await runner.exec([LAUNCHCTL, "print", target]);
1863
+ if (result.exitCode === NOT_FOUND)
1864
+ return { loaded: false };
1865
+ if (result.exitCode === NO_DOMAIN && result.stderr.includes(NO_DOMAIN_TEXT)) {
1866
+ return { domainMissing: true, loaded: false };
1867
+ }
1868
+ if (result.exitCode !== 0)
1869
+ throw new LaunchctlError(`print ${target}`, result.exitCode, result.stderr);
1870
+ return parsePrint(result.stdout);
1871
+ };
1872
+ var isDisabled = async (runner, domain, label) => {
1873
+ const result = await runner.exec([LAUNCHCTL, "print-disabled", domain]);
1874
+ if (result.exitCode !== 0) {
1875
+ throw new LaunchctlError(`print-disabled ${domain}`, result.exitCode, result.stderr);
1876
+ }
1877
+ return parseDisabled(result.stdout).has(label);
1878
+ };
1879
+ var bootstrap = async (runner, domain, plistPath) => {
1880
+ const result = await runner.exec([LAUNCHCTL, "bootstrap", domain, plistPath]);
1881
+ if (result.exitCode !== 0) {
1882
+ throw new LaunchctlError(`bootstrap ${domain} ${plistPath}`, result.exitCode, result.stderr);
1883
+ }
1884
+ };
1885
+ var waitUnloaded = async (runner, target, attempts = 120, intervalMs = 250) => {
1886
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
1887
+ if (!(await printService(runner, target)).loaded)
1888
+ return;
1889
+ await runner.sleep(intervalMs);
1890
+ }
1891
+ throw new Error(`${target} is still loaded ${String(attempts * intervalMs / 1000)}s after bootout`);
1892
+ };
1893
+ var bootoutIfLoaded = async (runner, target) => {
1894
+ if (!(await printService(runner, target)).loaded)
1895
+ return;
1896
+ const result = await runner.exec([LAUNCHCTL, "bootout", target]);
1897
+ try {
1898
+ await waitUnloaded(runner, target);
1899
+ } catch (cause) {
1900
+ if (result.exitCode === 0)
1901
+ throw cause;
1902
+ throw new LaunchctlError(`bootout ${target}`, result.exitCode, `${result.stderr.trim()} (and still loaded after waiting)`);
1903
+ }
1904
+ };
1905
+
1906
+ // src/launchd/sudo-allowlist.ts
1907
+ class SudoRefusedError extends Error {
1908
+ constructor(message) {
1909
+ super(message);
1910
+ this.name = "SudoRefusedError";
1911
+ }
1912
+ }
1913
+ var SUDO = "/usr/bin/sudo";
1914
+ var INSTALL = "/usr/bin/install";
1915
+ var RM = "/bin/rm";
1916
+ var WRITES = new Set(["bootstrap", "bootout", "kickstart"]);
1917
+ var octalMode = (mode) => Number.isSafeInteger(mode) && mode >= 0 && mode <= 4095 ? mode.toString(8).padStart(4, "0") : undefined;
1918
+ var checkPrefixes = (prefixes) => {
1919
+ if (prefixes.length === 0) {
1920
+ throw new Error("sudoRunner: no prefixes. Name the directories root may write, e.g. /Library/LaunchDaemons.");
1921
+ }
1922
+ for (const prefix of prefixes) {
1923
+ const found = prefix === "/" ? ['"/" would allow every path'] : pathProblems(prefix);
1924
+ if (found.length > 0) {
1925
+ throw new Error(`sudoRunner: prefix ${JSON.stringify(prefix)}: ${found.join("; ")}`);
1926
+ }
1927
+ }
1928
+ return [...prefixes];
1929
+ };
1930
+ var prefixOf = (path, prefixes) => pathProblems(path).length > 0 ? undefined : prefixes.find((prefix) => path.startsWith(`${prefix}/`));
1931
+ var labelOk = (label) => LABEL.test(label) && !RESERVED_LABEL_PREFIXES.some((prefix) => label.startsWith(prefix));
1932
+ var systemLabel = (target) => {
1933
+ const label = target?.startsWith("system/") === true ? target.slice("system/".length) : undefined;
1934
+ return label !== undefined && labelOk(label) ? label : undefined;
1935
+ };
1936
+ var daemonPlist = (label, prefixes) => {
1937
+ const path = plistPathFor(label, { kind: "system" }, undefined);
1938
+ return prefixOf(path, prefixes) === undefined ? undefined : path;
1939
+ };
1940
+ var NO_DAEMON_PREFIX = "needs /Library/LaunchDaemons among the prefixes, where the job’s plist is written and removed";
1941
+ var launchctlProblem = (args, prefixes) => {
1942
+ const [sub, ...rest] = args;
1943
+ if (sub === "bootstrap") {
1944
+ const given = rest[1] ?? "";
1945
+ const name = given.slice(given.lastIndexOf("/") + 1);
1946
+ const label = name.endsWith(".plist") ? name.slice(0, -".plist".length) : "";
1947
+ const shaped = rest.length === 2 && rest[0] === "system" && labelOk(label);
1948
+ if (!shaped)
1949
+ return "bootstrap takes exactly `system /Library/LaunchDaemons/<label>.plist`";
1950
+ const plist = daemonPlist(label, prefixes);
1951
+ if (plist === undefined)
1952
+ return `bootstrap ${NO_DAEMON_PREFIX}`;
1953
+ return given === plist ? undefined : `bootstrap loads only ${plist}, never another path`;
1954
+ }
1955
+ if (sub === "bootout") {
1956
+ const label = rest.length === 1 ? systemLabel(rest[0]) : undefined;
1957
+ if (label === undefined)
1958
+ return "bootout takes exactly `system/<label>`, never a bare domain";
1959
+ return daemonPlist(label, prefixes) === undefined ? `bootout ${NO_DAEMON_PREFIX}` : undefined;
1960
+ }
1961
+ if (sub === "kickstart") {
1962
+ const flags = rest.slice(0, -1);
1963
+ const flagsOk = flags.every((flag) => flag === "-k" || flag === "-p") && new Set(flags).size === flags.length;
1964
+ return flagsOk && systemLabel(rest[rest.length - 1]) !== undefined ? undefined : "kickstart takes only -k / -p, then `system/<label>`";
1965
+ }
1966
+ return `launchctl ${String(sub)} is never run as root`;
1967
+ };
1968
+ var ID = /^\d+$/;
1969
+ var idsOk = (ids) => {
1970
+ const [flag, id, second, secondId] = ids;
1971
+ if (ids.length === 0)
1972
+ return true;
1973
+ if (ids.length === 2)
1974
+ return (flag === "-o" || flag === "-g") && ID.test(id ?? "");
1975
+ return ids.length === 4 && flag === "-o" && ID.test(id ?? "") && second === "-g" && ID.test(secondId ?? "");
1976
+ };
1977
+ var privilegedProblem = (argv, context) => {
1978
+ const [program, ...args] = argv;
1979
+ const under = (path) => path !== undefined && prefixOf(path, context.prefixes) !== undefined;
1980
+ if (program === LAUNCHCTL)
1981
+ return launchctlProblem(args, context.prefixes);
1982
+ if (program === INSTALL) {
1983
+ const [safe, m, mode, ...rest] = args;
1984
+ if (safe !== "-S" || m !== "-m" || mode === undefined || !/^[0-7]{4}$/.test(mode)) {
1985
+ return "install takes exactly `-S -m <4 octal digits> [-o <uid>] [-g <gid>] <staged> <dest>`";
1986
+ }
1987
+ if (rest.length < 2 || !idsOk(rest.slice(0, -2)))
1988
+ return "install takes only -o <uid> -g <gid>";
1989
+ const [source, dest] = rest.slice(-2);
1990
+ if (source === undefined || source !== context.staged) {
1991
+ return "install copies only the file this runner staged";
1992
+ }
1993
+ return under(dest) ? undefined : "install destination is not under a declared prefix";
1994
+ }
1995
+ if (program === RM) {
1996
+ const [force, end, path, ...extra] = args;
1997
+ return force === "-f" && end === "--" && extra.length === 0 && under(path) ? undefined : "rm takes exactly `-f -- <path under a declared prefix>`";
1998
+ }
1999
+ return `${String(program)} is not on the sudo allowlist`;
2000
+ };
2001
+ var routeExec = (argv, operatorUid) => {
2002
+ const [program, sub] = argv;
2003
+ if (program === "sudo" || program?.endsWith("/sudo") === true) {
2004
+ return {
2005
+ refuse: "sudoRunner never passes a sudo argv through; it elevates only its allowlist"
2006
+ };
2007
+ }
2008
+ if (program !== LAUNCHCTL || sub === undefined || !WRITES.has(sub))
2009
+ return { as: "operator" };
2010
+ const target = sub === "kickstart" ? argv[argv.length - 1] : argv[2];
2011
+ if (target === "system" || target?.startsWith("system/") === true)
2012
+ return { as: "root" };
2013
+ const owner = /^(?:gui|user)\/(\d+)(?:\/|$)/.exec(target ?? "")?.[1];
2014
+ if (owner === undefined || Number(owner) === operatorUid)
2015
+ return { as: "operator" };
2016
+ return {
2017
+ refuse: `launchctl ${sub} ${String(target)}: that domain belongs to uid ${owner}, and sudoRunner ` + "elevates only the system domain. Deploy as that user, or as root."
2018
+ };
2019
+ };
2020
+
2021
+ // src/launchd/sudo-said.ts
2022
+ var PASSWORD = /^sudo: (?:a password is required|a terminal is required|sorry, you must have a tty)/m;
2023
+ var NOT_ALLOWED = /^(?:Sorry, user \S+ (?:is not allowed to execute|may not run sudo)|\S+ is not (?:in the sudoers file|allowed to run sudo on ))/m;
2024
+ var undeclared = (path, prefixes) => (cause) => {
2025
+ const code = errnoCode(cause);
2026
+ if (code !== "EACCES" && code !== "EPERM")
2027
+ throw cause;
2028
+ throw new Error(`sudoRunner ${path}: ${code} as the deploying user, and the path is under no declared prefix ` + `(${prefixes.join(", ")}). If root owns its directory, declare that directory as a prefix.`, { cause });
2029
+ };
2030
+ var sudoRefusal = (shown, result) => {
2031
+ if (result.exitCode !== 1)
2032
+ return;
2033
+ if (PASSWORD.test(result.stderr)) {
2034
+ return new SudoRefusedError(`sudo -n ${shown}: a password is required, and this runner never prompts. Run \`sudo -v\` ` + "in the deploying terminal just before the deploy, or grant exactly these commands " + "NOPASSWD (docs/launchd-sudo.md). The command did not run.");
2035
+ }
2036
+ if (NOT_ALLOWED.test(result.stderr)) {
2037
+ return new SudoRefusedError(`sudo -n ${shown}: sudoers does not let the deploying user run this. The command did not run.`);
2038
+ }
2039
+ return;
2040
+ };
2041
+
2042
+ // src/linux/sudo-allowlist-dir.ts
2043
+ var MKDIR = "/usr/bin/mkdir";
2044
+ var CHMOD = "/usr/bin/chmod";
2045
+ var CHOWN = "/usr/bin/chown";
2046
+ var RMDIR = "/usr/bin/rmdir";
2047
+ var parseDirMode = (text) => /^([0-7]{3}|[1-7][0-7]{3})$/.test(text) ? Number.parseInt(text, 8) : undefined;
2048
+ var DANGEROUS_MODE_BITS = 3090;
2049
+ var dirModeProblem = (mode) => {
2050
+ if (mode === undefined)
2051
+ return "mode";
2052
+ return (mode & DANGEROUS_MODE_BITS) !== 0 ? "a mode with setuid/setgid or group/other write (this runner installs only 0–0o777 minus 0o022, no special bits)" : undefined;
2053
+ };
2054
+ var ROOT_OWNER = /^(\+0|:\+0|\+0:\+0)$/;
2055
+ var dirProgramProblem = (program, args, prefixes) => {
2056
+ const under = (path2) => path2 !== undefined && prefixOf(path2, prefixes) !== undefined;
2057
+ if (program === MKDIR) {
2058
+ const [m, modeText, end2, path2, ...extra2] = args;
2059
+ const mode = modeText === undefined ? undefined : parseDirMode(modeText);
2060
+ if (m !== "-m" || mode === undefined || end2 !== "--" || extra2.length !== 0 || !under(path2)) {
2061
+ return "mkdir takes exactly `-m <octal> -- <path under a declared prefix>`";
2062
+ }
2063
+ return dirModeProblem(mode);
2064
+ }
2065
+ if (program === CHMOD) {
2066
+ const [modeText, end2, path2, ...extra2] = args;
2067
+ const mode = modeText === undefined ? undefined : parseDirMode(modeText);
2068
+ if (mode === undefined || end2 !== "--" || extra2.length !== 0 || !under(path2)) {
2069
+ return "chmod takes exactly `<octal> -- <path under a declared prefix>`";
2070
+ }
2071
+ return dirModeProblem(mode);
2072
+ }
2073
+ if (program === CHOWN) {
2074
+ const [owner, end2, path2, ...extra2] = args;
2075
+ return owner !== undefined && ROOT_OWNER.test(owner) && end2 === "--" && extra2.length === 0 && under(path2) ? undefined : "chown takes exactly `+0`, `:+0` or `+0:+0` -- <path under a declared prefix>` — root " + "only, forced numeric; a directory this runner elevates never hands ownership to anyone else";
2076
+ }
2077
+ const [end, path, ...extra] = args;
2078
+ return end === "--" && extra.length === 0 && under(path) ? undefined : "rmdir takes exactly `-- <path under a declared prefix>`";
2079
+ };
2080
+
2081
+ // src/linux/sudo-allowlist-chown.ts
2082
+ var forceNumericOwner = (spec) => {
2083
+ const [uidPart, gidPart] = spec.includes(":") ? spec.split(":") : [spec, undefined];
2084
+ const uid = uidPart === undefined || uidPart === "" ? "" : `+${uidPart}`;
2085
+ const gid = gidPart === undefined ? "" : `:+${gidPart}`;
2086
+ return `${uid}${gid}`;
2087
+ };
2088
+ var SAFE_OWNER = /^(\+\d+(:\+\d+)?|:\+\d+)$/;
2089
+ var fileChownProblem = (args, temp) => {
2090
+ const [owner, end, path, ...extra] = args;
2091
+ if (owner !== undefined && SAFE_OWNER.test(owner) && end === "--" && extra.length === 0 && path === temp) {
2092
+ return;
2093
+ }
2094
+ return "chown on the staged temp takes exactly `+<uid>`, `:+<gid>` or `+<uid>:+<gid>` -- " + "<the exact temp install just wrote>, numeric and +-forced";
2095
+ };
2096
+ var chownProblem = (args, context) => {
2097
+ const path = args[args.length - 1];
2098
+ if (context.temp !== undefined && path === context.temp)
2099
+ return fileChownProblem(args, context.temp);
2100
+ return dirProgramProblem(CHOWN, args, context.prefixes);
2101
+ };
2102
+ // src/linux/sudo-allowlist.ts
2103
+ var MV = "/usr/bin/mv";
2104
+ var RM2 = "/usr/bin/rm";
2105
+ var SYSTEMCTL_ABS = "/usr/bin/systemctl";
2106
+ var DIRECTORY_PROGRAMS = new Set([
2107
+ "mkdir",
2108
+ "chmod",
2109
+ "chown",
2110
+ "rmdir"
2111
+ ]);
2112
+ var DIRECTORY_PROGRAMS_ABS = new Set([MKDIR, CHMOD, CHOWN, RMDIR]);
2113
+ var ABS_OF = {
2114
+ chmod: CHMOD,
2115
+ chown: CHOWN,
2116
+ mkdir: MKDIR,
2117
+ rmdir: RMDIR,
2118
+ systemctl: SYSTEMCTL_ABS
2119
+ };
2120
+ var canonicalize = (argv) => {
2121
+ const abs = ABS_OF[argv[0] ?? ""];
2122
+ const program = abs ?? argv[0];
2123
+ if (program === CHOWN) {
2124
+ const [owner, ...rest] = argv.slice(1);
2125
+ return [CHOWN, owner === undefined ? "" : forceNumericOwner(owner), ...rest];
2126
+ }
2127
+ return abs === undefined ? argv : [abs, ...argv.slice(1)];
2128
+ };
2129
+ var dirOf = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
2130
+ var installProblem = (args, context) => {
2131
+ const [m, mode, t, dd, source, dest, ...extra] = args;
2132
+ if (m !== "-m" || mode === undefined || !/^[0-7]{4}$/.test(mode) || t !== "-T" || dd !== "--" || extra.length !== 0) {
2133
+ return "install takes exactly `-m <4 octal digits> -T -- <staged> <temp>`";
2134
+ }
2135
+ if (source === undefined || source !== context.staged) {
2136
+ return "install copies only the file this runner staged";
2137
+ }
2138
+ if (dest === undefined || dest !== context.temp) {
2139
+ return "install writes only the derived temp path for this write";
2140
+ }
2141
+ return prefixOf(dest, context.prefixes) === undefined ? "install destination is not under a declared prefix" : undefined;
2142
+ };
2143
+ var mvProblem = (args, context) => {
2144
+ const [f, t, dd, source, dest] = args;
2145
+ if (f !== "-f" || t !== "-T" || dd !== "--" || args.length !== 5) {
2146
+ return "mv takes exactly `-f -T -- <temp> <dest>`";
2147
+ }
2148
+ if (source === undefined || source !== context.temp) {
2149
+ return "mv moves only the temp path this runner just installed to";
2150
+ }
2151
+ if (dest === undefined || prefixOf(dest, context.prefixes) === undefined) {
2152
+ return "mv destination is not under a declared prefix";
2153
+ }
2154
+ return dirOf(source) === dirOf(dest) ? undefined : "mv must stay inside the destination’s directory";
2155
+ };
2156
+ var rmProblem = (args, context) => {
2157
+ const [force, end, path, ...extra] = args;
2158
+ return force === "-f" && end === "--" && extra.length === 0 && path !== undefined && prefixOf(path, context.prefixes) !== undefined ? undefined : "rm takes exactly `-f -- <path under a declared prefix>`";
2159
+ };
2160
+ var BANNED_SYSTEMCTL_FLAGS = new Set([
2161
+ "--user",
2162
+ "--global",
2163
+ "-H",
2164
+ "-M",
2165
+ "--root"
2166
+ ]);
2167
+ var UNIT_WRITE_VERBS = new Set([
2168
+ "enable",
2169
+ "disable",
2170
+ "start",
2171
+ "stop",
2172
+ "restart"
2173
+ ]);
2174
+ var systemctlProblem = (args, prefixes) => {
2175
+ if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {
2176
+ return "systemctl --user/--global/-H/-M/--root is never run as root";
2177
+ }
2178
+ const [verb, ...rest] = args;
2179
+ if (verb === "daemon-reload")
2180
+ return rest.length === 0 ? undefined : "daemon-reload takes no operands";
2181
+ if (verb !== undefined && UNIT_WRITE_VERBS.has(verb)) {
2182
+ const [dd, unit, ...extra] = rest;
2183
+ return dd === "--" && unit !== undefined && NAME.test(unit) && extra.length === 0 ? undefined : `${verb} takes exactly \`-- <unit>\``;
2184
+ }
2185
+ return `systemctl ${String(verb)} is never run as root`;
2186
+ };
2187
+ var privilegedProblem2 = (argv, context) => {
2188
+ const [program, ...args] = argv;
2189
+ if (program === INSTALL)
2190
+ return installProblem(args, context);
2191
+ if (program === MV)
2192
+ return mvProblem(args, context);
2193
+ if (program === RM2)
2194
+ return rmProblem(args, context);
2195
+ if (program === SYSTEMCTL_ABS)
2196
+ return systemctlProblem(args, context.prefixes);
2197
+ if (program === CHOWN)
2198
+ return chownProblem(args, context);
2199
+ if (program === MKDIR || program === CHMOD || program === RMDIR) {
2200
+ return dirProgramProblem(program, args, context.prefixes);
2201
+ }
2202
+ return `${String(program)} is not on the sudo allowlist`;
2203
+ };
2204
+ var routeExec2 = (argv, prefixes) => {
2205
+ const [program, ...args] = argv;
2206
+ if (program === "sudo" || program?.endsWith("/sudo") === true) {
2207
+ return {
2208
+ refuse: "sshSudoRunner never passes a sudo argv through; it elevates only its allowlist"
2209
+ };
2210
+ }
2211
+ if (program === "systemctl" || program === SYSTEMCTL_ABS) {
2212
+ if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {
2213
+ return { refuse: "sshSudoRunner refuses systemctl --user/--global/-H/-M/--root" };
2214
+ }
2215
+ const [verb] = args;
2216
+ return verb === "daemon-reload" || verb !== undefined && UNIT_WRITE_VERBS.has(verb) ? { as: "root" } : { as: "operator" };
2217
+ }
2218
+ if (DIRECTORY_PROGRAMS.has(program ?? "") || DIRECTORY_PROGRAMS_ABS.has(program ?? "")) {
2219
+ const path = args[args.length - 1];
2220
+ return path !== undefined && prefixOf(path, prefixes) !== undefined ? { as: "root" } : { as: "operator" };
2221
+ }
2222
+ return { as: "operator" };
2223
+ };
2224
+
2225
+ // src/launchd/sudo-acl.ts
2226
+ var LS = "/bin/ls";
2227
+ var WRITE_RIGHTS = new Set([
2228
+ "add_file",
2229
+ "add_subdirectory",
2230
+ "append",
2231
+ "chown",
2232
+ "delete",
2233
+ "delete_child",
2234
+ "write",
2235
+ "writesecurity"
2236
+ ]);
2237
+ var ENTRY = /^\s*\d+:\s+\S+\s+(?:inherited\s+)?allow\s+(\S+)/;
2238
+ var aclGrants = (listing) => listing.split(`
2239
+ `).filter((line) => {
2240
+ const rights = ENTRY.exec(line)?.[1];
2241
+ return rights !== undefined && rights.split(",").some((right) => WRITE_RIGHTS.has(right));
2242
+ }).map((line) => line.trim());
2243
+ var assertNoAclGrants = async (base, path, chain) => {
2244
+ const listed = await base.exec([LS, "-lden", "--", ...chain]);
2245
+ if (listed.exitCode !== 0) {
2246
+ throw new SudoRefusedError(`sudoRunner ${path}: could not read the ACLs of the directories from / down to it ` + `(${LS} -lden exit ${String(listed.exitCode)}). Nothing ran as root.`);
2247
+ }
2248
+ const grants = aclGrants(listed.stdout);
2249
+ if (grants.length === 0)
2250
+ return;
2251
+ throw new SudoRefusedError(`sudoRunner ${path}: a directory from / down to it carries an ACL that lets someone other ` + `than root change what it holds (${grants.join("; ")}). Remove the entry (chmod -a), or ` + "declare a prefix without one. Nothing ran as root.");
2252
+ };
2253
+
2254
+ // src/launchd/sudo-guard-mode.ts
2255
+ var refuse5 = (path, message) => new SudoRefusedError(`sudoRunner ${path}: ${message}`);
2256
+ var modeProblem = (options) => {
2257
+ const rootAdjacent = options.uid === undefined || options.uid === 0 || options.gid === undefined || options.gid === 0;
2258
+ if (!rootAdjacent)
2259
+ return;
2260
+ const found = [];
2261
+ if ((options.mode & 3072) !== 0)
2262
+ found.push("setuid/setgid");
2263
+ if ((options.mode & 18) !== 0)
2264
+ found.push("writable by group or other");
2265
+ if (found.length === 0)
2266
+ return;
2267
+ return `mode ${options.mode.toString(8).padStart(4, "0")} is ${found.join(" and ")} on a root-owned file`;
2268
+ };
2269
+ var canRead = (file, uid, groups) => {
2270
+ if (uid === 0)
2271
+ return true;
2272
+ if (file.uid === uid)
2273
+ return (file.mode & 256) !== 0;
2274
+ const groupRead = (file.mode & 32) !== 0;
2275
+ const otherRead = (file.mode & 4) !== 0;
2276
+ if (groupRead === otherRead)
2277
+ return groupRead;
2278
+ if (groups === undefined)
2279
+ return;
2280
+ return groups.includes(file.gid) ? groupRead : otherRead;
2281
+ };
2282
+ var assertReadableBack = async (base, path, options, groups) => {
2283
+ const operator = base.effectiveUid();
2284
+ const parent = path.slice(0, path.lastIndexOf("/")) || "/";
2285
+ const gid = options.gid ?? (await base.stat(parent))?.gid ?? 0;
2286
+ const file = { gid, mode: options.mode, uid: options.uid ?? 0 };
2287
+ if (canRead(file, operator, groups) !== false)
2288
+ return;
2289
+ throw refuse5(path, `owner ${String(file.uid)}, group ${String(gid)}, mode ${file.mode.toString(8)} would hide it ` + `from the deploying user (uid ${String(operator)}), who reads every file back. Declare a ` + "mode that user can read, or deploy as root. Nothing was written.");
2290
+ };
2291
+ var operatorGroups = async (base) => {
2292
+ const result = await base.exec(["/usr/bin/id", "-G"]);
2293
+ const ids = result.stdout.trim().split(/\s+/);
2294
+ return result.exitCode === 0 && ids.every((id) => /^\d+$/.test(id)) ? ids.map(Number) : undefined;
2295
+ };
2296
+
2297
+ // src/launchd/sudo-guard.ts
2298
+ var refuse6 = (path, message) => new SudoRefusedError(`sudoRunner ${path}: ${message}`);
2299
+ var betweenDirs = (prefix, path) => {
2300
+ const parts = path.slice(prefix.length + 1).split("/").slice(0, -1);
2301
+ return parts.map((_, index) => `${prefix}/${parts.slice(0, index + 1).join("/")}`);
2302
+ };
2303
+ var aboveDirs = (prefix) => {
2304
+ const parts = prefix.split("/").slice(1, -1);
2305
+ return ["/", ...parts.map((_, index) => `/${parts.slice(0, index + 1).join("/")}`)];
2306
+ };
2307
+ var rootOnlyProblem = (stat) => {
2308
+ if (stat === undefined)
2309
+ return "is missing";
2310
+ if (stat.kind !== "directory")
2311
+ return `is a ${stat.kind}, not a directory`;
2312
+ if (stat.uid !== 0)
2313
+ return `is owned by uid ${String(stat.uid)}, not root`;
2314
+ if ((stat.mode & 18) !== 0) {
2315
+ return `is writable by group or other (mode ${stat.mode.toString(8).padStart(4, "0")})`;
2316
+ }
2317
+ return;
2318
+ };
2319
+ var assertPlainPath = async (base, prefix, path, expect) => {
2320
+ const above = aboveDirs(prefix);
2321
+ for (const [index, stat] of (await Promise.all(above.map((dir) => base.stat(dir)))).entries()) {
2322
+ const up = stat?.kind === "symlink" && stat.uid === 0 ? undefined : rootOnlyProblem(stat);
2323
+ if (up !== undefined) {
2324
+ throw refuse6(path, `${String(above[index])}, above the declared prefix ${prefix}, ${up}. Whoever may change it ` + "may swap the prefix itself; every directory from / down must be root-only. Nothing ran as root.");
2325
+ }
2326
+ }
2327
+ const dirs = betweenDirs(prefix, path);
2328
+ const [prefixStat, ...stats] = await Promise.all([prefix, ...dirs].map((dir) => base.stat(dir)));
2329
+ const bad = rootOnlyProblem(prefixStat);
2330
+ if (bad !== undefined) {
2331
+ throw refuse6(path, `the declared prefix ${prefix} ${bad}. Declare only a real directory that root owns and ` + "only root may write. Nothing ran as root.");
2332
+ }
2333
+ for (const [index, stat] of stats.entries()) {
2334
+ const dir = String(dirs[index]);
2335
+ if (stat?.kind !== "directory") {
2336
+ throw refuse6(path, `${dir} is ${stat === undefined ? "missing" : `a ${stat.kind}`}; root ` + "writes only through real directories under a declared prefix.");
2337
+ }
2338
+ const below = rootOnlyProblem(stat);
2339
+ if (below !== undefined) {
2340
+ throw refuse6(path, `${dir} ${below}. Every directory from the prefix ${prefix} down to the file must be one ` + "that root owns and only root may write. Nothing ran as root.");
2341
+ }
2342
+ }
2343
+ await assertNoAclGrants(base, path, [...above, prefix, ...dirs]);
2344
+ const target = await base.stat(path);
2345
+ if (target === undefined && expect === "file-or-absent")
2346
+ return "absent";
2347
+ if (target?.kind === "file")
2348
+ return "file";
2349
+ throw refuse6(path, `is ${target === undefined ? "missing" : `a ${target.kind}`}, not a file`);
2350
+ };
2351
+ var assertInstallable = async (base, prefix, path, options, groups) => {
2352
+ const mode = modeProblem(options);
2353
+ if (mode !== undefined) {
2354
+ throw refuse6(path, `${mode}. Declare a mode without those bits (0644, 0640, 0755), or give the file to its ` + "real owner. Nothing ran as root.");
2355
+ }
2356
+ await assertPlainPath(base, prefix, path, "file-or-absent");
2357
+ await assertReadableBack(base, path, options, groups);
2358
+ };
2359
+
2360
+ // src/linux/sudo-listing.ts
2361
+ var KINDS2 = new Map([
2362
+ ["-", "file"],
2363
+ ["d", "directory"],
2364
+ ["l", "symlink"]
2365
+ ]);
2366
+ var permBits = (perms) => {
2367
+ if (perms.length !== 9)
2368
+ return;
2369
+ let mode = 0;
2370
+ for (const index of [0, 1, 2]) {
2371
+ const at = index * 3;
2372
+ const shift = 6 - at;
2373
+ const [read, write, exec] = [perms[at], perms[at + 1], perms[at + 2]];
2374
+ if (read !== "r" && read !== "-" || write !== "w" && write !== "-")
2375
+ return;
2376
+ if (read === "r")
2377
+ mode |= 4 << shift;
2378
+ if (write === "w")
2379
+ mode |= 2 << shift;
2380
+ const [set, unset] = index === 2 ? ["t", "T"] : ["s", "S"];
2381
+ if (exec === "x" || exec === set)
2382
+ mode |= 1 << shift;
2383
+ else if (exec !== "-" && exec !== unset)
2384
+ return;
2385
+ if (exec === set || exec === unset)
2386
+ mode |= 2048 >> index;
2387
+ }
2388
+ return mode;
2389
+ };
2390
+ var LINE = /^([-dlbcps])(\S{9})([+.@]?)\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/;
2391
+ var nameOf = (tail, paths) => [...paths].sort((left, right) => right.length - left.length).find((path) => tail.endsWith(` ${path}`) || tail.includes(` ${path} -> `));
2392
+ var parseListing = (stdout, paths) => {
2393
+ const entries = new Map;
2394
+ for (const line of stdout.split(`
2395
+ `).filter((text) => text.trim() !== "")) {
2396
+ const match = LINE.exec(line);
2397
+ if (match === null)
2398
+ return;
2399
+ const [, type, perms, flag, uid, gid, size, tail] = match;
2400
+ const mode = permBits(perms ?? "");
2401
+ const path = nameOf(tail ?? "", paths);
2402
+ if (mode === undefined || path === undefined || entries.has(path))
2403
+ return;
2404
+ entries.set(path, {
2405
+ acl: flag === "+",
2406
+ gid: Number(gid),
2407
+ kind: KINDS2.get(type ?? "") ?? "other",
2408
+ mode,
2409
+ size: Number(size),
2410
+ uid: Number(uid)
2411
+ });
2412
+ }
2413
+ return entries.size === paths.length ? entries : undefined;
2414
+ };
2415
+ var readListing = async (base, ls, what, paths) => {
2416
+ const result = await base.exec([ls, "-ldn", "--", ...paths]);
2417
+ const entries = result.exitCode === 0 ? parseListing(result.stdout, paths) : undefined;
2418
+ if (entries === undefined) {
2419
+ throw new SudoRefusedError(`sudoRunner ${what}: could not read ${paths.join(", ")} (${ls} -ldn exit ` + `${String(result.exitCode)}: ${result.stderr.trim().slice(0, 200)}). Nothing ran as root.`);
2420
+ }
2421
+ return entries;
2422
+ };
2423
+
2424
+ // src/linux/sudo-guard.ts
2425
+ var refuse7 = (path, message) => new SudoRefusedError(`sshSudoRunner ${path}: ${message}`);
2426
+ var rootOnlyProblem2 = (entry) => {
2427
+ if (entry === undefined)
2428
+ return "is missing";
2429
+ if (entry.kind !== "directory")
2430
+ return `is a ${entry.kind}, not a directory`;
2431
+ if (entry.uid !== 0)
2432
+ return `is owned by uid ${String(entry.uid)}, not root`;
2433
+ if ((entry.mode & 18) !== 0) {
2434
+ return `is writable by group or other (mode ${entry.mode.toString(8).padStart(4, "0")})`;
2435
+ }
2436
+ if (entry.acl)
2437
+ return "carries a POSIX ACL this runner cannot verify the grants of (ls -ldn `+`)";
2438
+ return;
2439
+ };
2440
+ var targetProblem = (entry, expect) => {
2441
+ if (expect === "absent")
2442
+ return entry === undefined ? undefined : `already exists (a ${entry.kind})`;
2443
+ if (entry === undefined)
2444
+ return expect === "file-or-absent" ? undefined : "is missing";
2445
+ if (expect === "file-or-absent")
2446
+ return entry.kind === "file" ? undefined : `is a ${entry.kind}, not a file`;
2447
+ return entry.kind === expect ? undefined : `is a ${entry.kind}, not a ${expect}`;
2448
+ };
2449
+ var assertGuardedChain = async (base, prefix, path, expect) => {
2450
+ const above = aboveDirs(prefix);
2451
+ const between = betweenDirs(prefix, path);
2452
+ const chain = [...above, prefix, ...between];
2453
+ const listed = await readListing(base, "/usr/bin/ls", path, chain);
2454
+ for (const dir of above) {
2455
+ const entry = listed.get(dir);
2456
+ const problem2 = entry?.kind === "symlink" && entry.uid === 0 ? undefined : rootOnlyProblem2(entry);
2457
+ if (problem2 !== undefined) {
2458
+ throw refuse7(path, `${dir}, above the declared prefix ${prefix}, ${problem2}. Whoever may change it may swap ` + "the prefix itself; every directory from / down must be root-only. Nothing ran as root.");
2459
+ }
2460
+ }
2461
+ const prefixProblem = rootOnlyProblem2(listed.get(prefix));
2462
+ if (prefixProblem !== undefined) {
2463
+ throw refuse7(path, `the declared prefix ${prefix} ${prefixProblem}. Declare only a real directory that root ` + "owns and only root may write. Nothing ran as root.");
2464
+ }
2465
+ for (const dir of between) {
2466
+ const problem2 = rootOnlyProblem2(listed.get(dir));
2467
+ if (problem2 !== undefined) {
2468
+ throw refuse7(path, `${dir} ${problem2}. Every directory from the prefix ${prefix} down to the file must be ` + "one that root owns and only root may write. Nothing ran as root.");
2469
+ }
2470
+ }
2471
+ const target = await base.stat(path);
2472
+ const problem = targetProblem(target, expect);
2473
+ if (problem !== undefined)
2474
+ throw refuse7(path, `${problem}. Nothing ran as root.`);
2475
+ return target;
2476
+ };
2477
+ var assertInstallable2 = async (base, prefix, path, options, groups) => {
2478
+ const mode = modeProblem(options);
2479
+ if (mode !== undefined) {
2480
+ throw refuse7(path, `${mode}. Declare a mode without those bits (0644, 0640, 0755), or give the file to its ` + "real owner. Nothing ran as root.");
2481
+ }
2482
+ await assertGuardedChain(base, prefix, path, "file-or-absent");
2483
+ await assertReadableBack(base, path, options, groups);
2484
+ };
2485
+
2486
+ // src/linux/sudo-exec.ts
2487
+ var DIR_EXPECT = {
2488
+ chmod: "directory",
2489
+ chown: "directory",
2490
+ mkdir: "absent",
2491
+ rmdir: "directory"
2492
+ };
2493
+ var EMPTY_FRAGMENT_OK = new Set(["stop", "disable"]);
2494
+ var unitStatusOf = async (base, unit) => {
2495
+ const result = await base.exec([
2496
+ SYSTEMCTL_ABS,
2497
+ "show",
2498
+ "--no-pager",
2499
+ "-p",
2500
+ "FragmentPath,LoadState",
2501
+ "--",
2502
+ unit
2503
+ ]);
2504
+ if (result.exitCode !== 0) {
2505
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: could not read FragmentPath/LoadState (systemctl show exit ` + `${String(result.exitCode)}). Nothing ran as root.`);
2506
+ }
2507
+ const fields = parseShow(result.stdout);
2508
+ return {
2509
+ fragmentPath: fields.get("FragmentPath") ?? "",
2510
+ loadState: fields.get("LoadState") ?? ""
2511
+ };
2512
+ };
2513
+ var elevatedExec = (base, elevate, prefixes, argv) => {
2514
+ const canonical = canonicalize(argv);
2515
+ const expect = DIR_EXPECT[argv[0] ?? ""];
2516
+ if (expect !== undefined) {
2517
+ const path = argv[argv.length - 1] ?? "";
2518
+ const prefix = prefixOf(path, prefixes);
2519
+ return elevate(canonical, {}, async () => {
2520
+ if (prefix === undefined) {
2521
+ throw new SudoRefusedError(`sshSudoRunner ${path}: not under a declared prefix. Nothing ran as root.`);
2522
+ }
2523
+ await assertGuardedChain(base, prefix, path, expect);
2524
+ });
2525
+ }
2526
+ if (canonical[0] !== SYSTEMCTL_ABS || canonical[1] === "daemon-reload")
2527
+ return elevate(canonical);
2528
+ const verb = canonical[1] ?? "";
2529
+ const unit = canonical[3] ?? "";
2530
+ return elevate(canonical, {}, async () => {
2531
+ const { fragmentPath, loadState } = await unitStatusOf(base, unit);
2532
+ if (loadState === "masked") {
2533
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: is masked. Masking is someone's decision, not drift; unmask it ` + "deliberately, then redeploy. Nothing ran as root.");
2534
+ }
2535
+ if (fragmentPath === "") {
2536
+ if (EMPTY_FRAGMENT_OK.has(verb))
2537
+ return;
2538
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: has no unit file (FragmentPath empty), and ${verb} is not ` + "stop/disable — refusing rather than run it against whatever systemd currently thinks " + `${unit} is (a kernel-generated unit, say). Nothing ran as root.`);
2539
+ }
2540
+ if (prefixOf(fragmentPath, prefixes) === undefined) {
2541
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: FragmentPath ${fragmentPath} is outside every declared prefix ` + `(${prefixes.join(", ")}); refusing to touch a unit this runner does not own. Nothing ran as root.`);
2542
+ }
2543
+ });
2544
+ };
2545
+
2546
+ // src/linux/sudo-stage.ts
2547
+ var refuse8 = (message) => new Error(`sshSudoRunner stage: ${message}`);
2548
+ var stageRemote = (base) => async (bytes) => {
2549
+ const made = await base.exec(["/usr/bin/mktemp", "-d"]);
2550
+ if (made.exitCode !== 0)
2551
+ throw refuse8(`mktemp -d -> ${String(made.exitCode)}: ${made.stderr.trim()}`);
2552
+ const dir = made.stdout.trim();
2553
+ if (dir === "" || !dir.startsWith("/")) {
2554
+ throw refuse8(`mktemp -d printed ${JSON.stringify(made.stdout)}, not an absolute path`);
2555
+ }
2556
+ const dispose = async () => {
2557
+ const removed = await base.exec(["/usr/bin/rm", "-rf", "--", dir]);
2558
+ if (removed.exitCode !== 0) {
2559
+ process.stderr.write(`sshSudoRunner: could not remove staging dir ${dir}: ${removed.stderr.trim()}
2560
+ `);
2561
+ }
2562
+ };
2563
+ try {
2564
+ const path = `${dir}/staged`;
2565
+ await base.writeFileAtomic(path, bytes, { mode: 384 });
2566
+ return { dispose, path };
2567
+ } catch (cause) {
2568
+ await dispose();
2569
+ throw cause;
2570
+ }
2571
+ };
2572
+
2573
+ // src/linux/sudo-write.ts
2574
+ import { randomBytes as randomBytes3 } from "node:crypto";
2575
+ var basename2 = (path) => path.slice(path.lastIndexOf("/") + 1);
2576
+ var dirOf2 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
2577
+ var deriveTemp = (dest) => `${dirOf2(dest)}/.${basename2(dest)}.hf-${randomBytes3(6).toString("hex")}.tmp`;
2578
+ var mustSucceed = (result, argv) => {
2579
+ if (result.exitCode !== 0) {
2580
+ throw new Error(`sudo -n ${JSON.stringify(argv)} -> ${String(result.exitCode)}: ${result.stderr.trim().slice(0, 300)}`);
2581
+ }
2582
+ };
2583
+ var describeCleanupFailure = (cleaned) => {
2584
+ if (typeof cleaned === "object" && cleaned !== null && "exitCode" in cleaned && "stderr" in cleaned) {
2585
+ const result = cleaned;
2586
+ return `exit ${String(result.exitCode)}: ${result.stderr.trim().slice(0, 300)}`;
2587
+ }
2588
+ return cleaned instanceof Error ? cleaned.message : String(cleaned);
2589
+ };
2590
+ var vetWrite = async (base, prefixes, path, options) => {
2591
+ const prefix = prefixOf(path, prefixes);
2592
+ if (prefix === undefined)
2593
+ return;
2594
+ const mode = octalMode(options.mode);
2595
+ if (mode === undefined)
2596
+ throw new SudoRefusedError(`sshSudoRunner ${path}: mode must be 0–0o7777`);
2597
+ const groups = await operatorGroups(base);
2598
+ await assertInstallable2(base, prefix, path, options, groups);
2599
+ return { mode, prefix };
2600
+ };
2601
+ var safeOwnerSpec = (uid, gid) => `${uid === undefined ? "" : `+${uid}`}${gid === undefined ? "" : `:+${gid}`}`;
2602
+ var chownNeeded = (options) => {
2603
+ const uid = options.uid !== undefined && options.uid !== 0 ? options.uid : undefined;
2604
+ const gid = options.gid !== undefined && options.gid !== 0 ? options.gid : undefined;
2605
+ return uid === undefined && gid === undefined ? undefined : { ...uid === undefined ? {} : { uid }, ...gid === undefined ? {} : { gid } };
2606
+ };
2607
+ var writeUnderPrefix = async (base, elevate, stage, path, bytes, options, mode) => {
2608
+ const temp = deriveTemp(path);
2609
+ const staged = await stage(bytes);
2610
+ try {
2611
+ const installArgv = [INSTALL, "-m", mode, "-T", "--", staged.path, temp];
2612
+ const before = async () => {
2613
+ const existing = await base.stat(temp);
2614
+ if (existing !== undefined) {
2615
+ throw new SudoRefusedError(`sshSudoRunner ${temp}: already exists (a ${existing.kind}); refusing to install over ` + "it. Nothing ran as root.");
2616
+ }
2617
+ };
2618
+ mustSucceed(await elevate(installArgv, { staged: staged.path, temp }, before), installArgv);
2619
+ try {
2620
+ const owner = chownNeeded(options);
2621
+ if (owner !== undefined) {
2622
+ const chownArgv = [CHOWN, safeOwnerSpec(owner.uid, owner.gid), "--", temp];
2623
+ mustSucceed(await elevate(chownArgv, { temp }), chownArgv);
2624
+ }
2625
+ const mvArgv = [MV, "-f", "-T", "--", temp, path];
2626
+ mustSucceed(await elevate(mvArgv, { temp }), mvArgv);
2627
+ } catch (cause) {
2628
+ const rmArgv = [RM2, "-f", "--", temp];
2629
+ const cleaned = await elevate(rmArgv, { temp }).then((result) => result.exitCode === 0 ? undefined : result).catch((rmCause) => rmCause);
2630
+ if (cleaned !== undefined) {
2631
+ throw new Error(`${cause instanceof Error ? cause.message : String(cause)} — AND removing the leftover ` + `temp ${temp} also failed (${describeCleanupFailure(cleaned)}); it needs manual ` + "cleanup as root.", { cause });
2632
+ }
2633
+ throw cause;
2634
+ }
2635
+ } finally {
2636
+ await staged.dispose();
2637
+ }
2638
+ };
2639
+
2640
+ // src/linux/sudo-runner.ts
2641
+ var outsidePrefixHint = (path, prefixes) => (cause) => {
2642
+ const message = cause instanceof Error ? cause.message : String(cause);
2643
+ if (!/permission denied|operation not permitted/i.test(message))
2644
+ throw cause;
2645
+ throw new Error(`sshSudoRunner ${path}: the operator could not do this, and the path is under no declared ` + `prefix (${prefixes.join(", ")}). If root owns its directory, declare that directory as ` + "a prefix.", { cause });
2646
+ };
2647
+ var makeSshSudoRunner = (prefixList, deps) => {
2648
+ const prefixes = checkPrefixes(prefixList);
2649
+ const { base } = deps;
2650
+ const elevate = async (argv, context, before) => {
2651
+ const shown = JSON.stringify(argv);
2652
+ const problem = privilegedProblem2(argv, { prefixes, ...context });
2653
+ if (problem !== undefined)
2654
+ throw new SudoRefusedError(`sshSudoRunner refused ${shown}: ${problem}`);
2655
+ await before?.();
2656
+ deps.log(`homeflare/linux sudo -n ${shown}`);
2657
+ const result = await base.exec([SUDO, "-n", "--", ...argv]);
2658
+ const refused = sudoRefusal(shown, result);
2659
+ if (refused !== undefined)
2660
+ throw refused;
2661
+ return result;
2662
+ };
2663
+ return {
2664
+ checkWrite: async (path, options) => {
2665
+ await vetWrite(base, prefixes, path, options);
2666
+ },
2667
+ effectiveUid: () => base.effectiveUid(),
2668
+ exec: async (argv) => {
2669
+ const route = routeExec2(argv, prefixes);
2670
+ if ("refuse" in route)
2671
+ throw new SudoRefusedError(`sshSudoRunner: ${route.refuse}`);
2672
+ if (route.as === "operator")
2673
+ return base.exec(argv);
2674
+ return elevatedExec(base, elevate, prefixes, argv);
2675
+ },
2676
+ lookupGroup: (nameOrId) => base.lookupGroup(nameOrId),
2677
+ lookupUser: (nameOrId) => base.lookupUser(nameOrId),
2678
+ privileged: true,
2679
+ readFile: (path) => base.readFile(path),
2680
+ removeFile: async (path) => {
2681
+ const prefix = prefixOf(path, prefixes);
2682
+ if (prefix === undefined)
2683
+ return base.removeFile(path).catch(outsidePrefixHint(path, prefixes));
2684
+ const target = await assertGuardedChain(base, prefix, path, "file-or-absent");
2685
+ if (target === undefined)
2686
+ return;
2687
+ const argv = [RM2, "-f", "--", path];
2688
+ const result = await elevate(argv);
2689
+ if (result.exitCode !== 0) {
2690
+ throw new Error(`sudo -n ${JSON.stringify(argv)} -> ${String(result.exitCode)}: ${result.stderr.trim().slice(0, 300)}`);
2691
+ }
2692
+ },
2693
+ sleep: (ms) => base.sleep(ms),
2694
+ stat: (path) => base.stat(path),
2695
+ writeFileAtomic: async (path, bytes, options) => {
2696
+ const vetted = await vetWrite(base, prefixes, path, options);
2697
+ if (vetted === undefined) {
2698
+ return base.writeFileAtomic(path, bytes, options).catch(outsidePrefixHint(path, prefixes));
2699
+ }
2700
+ await writeUnderPrefix(base, elevate, deps.stage, path, bytes, options, vetted.mode);
2701
+ }
2702
+ };
2703
+ };
2704
+ var REQUIRED_PROGRAMS = [SUDO, INSTALL, MV, RM2, MKDIR, CHMOD, CHOWN, RMDIR, SYSTEMCTL_ABS];
2705
+ var checkProgramsExist = async (base, host) => {
2706
+ const script = REQUIRED_PROGRAMS.map((p) => `test -x '${p}' || { echo "$0: ${p}"; exit 1; }`).join(" && ");
2707
+ const result = await base.exec(["/bin/sh", "-c", script, "sshSudoRunner"]);
2708
+ if (result.exitCode !== 0) {
2709
+ throw new Error(`sshSudoRunner ${host}: ${result.stdout.trim() || result.stderr.trim()} is missing or not ` + "executable on this host. sshSudoRunner sends absolute GNU coreutils and systemd paths.");
2710
+ }
2711
+ };
2712
+ var sshSudoRunner = async (options) => {
2713
+ const base = await sshRunner(options);
2714
+ await checkProgramsExist(base, options.host);
2715
+ return makeSshSudoRunner(options.prefixes, {
2716
+ base,
2717
+ log: options.log ?? ((line) => {
2718
+ process.stderr.write(`${line}
2719
+ `);
2720
+ }),
2721
+ stage: stageRemote(base)
2722
+ });
2723
+ };
1593
2724
  export {
1594
2725
  DEFAULT_UNIT_DIRECTORY,
1595
2726
  HostDirectory,
@@ -1605,8 +2736,9 @@ export {
1605
2736
  hostRunnerLayer,
1606
2737
  linuxProviders,
1607
2738
  renderUnit,
1608
- sshRunner
2739
+ sshRunner,
2740
+ sshSudoRunner
1609
2741
  };
1610
2742
 
1611
- //# debugId=EAA28AD7F62613BA64756E2164756E21
2743
+ //# debugId=19CF970890A2548664756E2164756E21
1612
2744
  //# sourceMappingURL=index.js.map