import { readdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { osUserHome } from './os-user'; // Checking that a member's Linux account actually WORKS, rather than that provisioning reported success. // // ── Why this exists ── // // Every step in `provisionOsAccount` is deliberately non-fatal: a member whose account provisioned fine // should not be rolled back because their shell config failed. That is right, and it has a cost — the // failures accumulate silently, and the owner learns about them from a file browser saying "Officer // cannot read this folder", which is three steps removed from the cause. // // The 2026-08-19 case was the sharp version. Every named ACL entry was correct. The Linux user existed. // The home existed. The grants were right. And the platform still could not read the home, because a // later `chmod 700` had recomputed the ACL mask to `---` and clamped the entries to nothing. Nothing in // the pipeline was in a position to notice, because nothing tried the thing that actually matters. // // So: try it. `readdir` on the member's home, as the process that will do it for real. It is the same // call the file browser makes, which is the point — a check that tests something ADJACENT to the failure // is how you get a green report and a broken server. // // This is the same posture as `assertPermissionTotality` and `assertInstallLayout`: state the invariant, // verify it, and say plainly what is wrong. The difference is that those refuse to boot and this does // not — a member with a broken home must not stop the platform serving everyone else. export type OsAccountCheck = { email: string; osUser: string | null; home: string; ok: boolean; /** Null when `ok`. Otherwise what is wrong and what fixes it. */ problem: string | null; }; /** * Can the platform actually read this member's home? * * Deliberately not a permissions inspection. Reading `getfacl` and reasoning about masks is how this was * missed in the first place: the ACL *looked* right. The only trustworthy question is whether the call * the file browser makes succeeds. */ export async function checkOsAccount(email: string, osUser: string | null): Promise { const home = osUserHome(email); const base = { email, osUser, home }; if (!osUser) { return { ...base, ok: false, problem: 'no Linux user — every `confined` permission (files, terminal, chat) is dropped for this account, ' + 'whatever the role grants. Fix: POST /api/users//provision-linux, or Settings → User management.', }; } if (!existsSync(home)) { return { ...base, ok: false, problem: `home does not exist at ${home}. Fix: re-provision the Linux account.` }; } try { await readdir(home); return { ...base, ok: true, problem: null }; } catch (ex) { const code = (ex as { code?: string }).code; if (code === 'EACCES' || code === 'EPERM') { return { ...base, ok: false, problem: `the platform cannot read ${home} (${code}). The named ACL entries may look correct and still ` + `grant nothing: a chmod recomputes the ACL mask from the group bits, so a directory closed to ` + `700 gets \`mask::---\` and clamps every entry to \`#effective:---\`. ` + `Fix: re-provision the Linux account, or \`sudo setfacl -R -m m::rwx -m d:m::rwx '${home}'\`.`, }; } return { ...base, ok: false, problem: `could not read ${home}: ${ex instanceof Error ? ex.message : ex}` }; } }