Files
jason.woltje 63e2a574af fix(discord): a native fs error never prints its path: CLI choke point and journal labels (#1873)
The discord CLI's top-level handler prints a native Node error that is not
already a labelled refusal as 'cannot <syscall> <label> (<CODE>)'. The journal
and approvals readers and writers name the file by label at the source, so
conversation, control-board and webui callers get the label too. The session
directory check moves to engine-pi.mjs as sessionContinues(). Exit codes and
refusal codes are unchanged.
2026-11-11 10:11:11 -06:00

54 lines
2.9 KiB
JavaScript

// One error class for the package. exitCode follows docs/TOOLS.md: 1 operation
// failed, 2 invalid data or configuration, 3 refused by a brake (STOP or a
// held binding; a supervisor must not retry), 4 usage.
// A refusal that names an unknown key quotes it only when it is a plain name;
// a key that could carry a host path or a secret is described, never echoed (#1873).
export const plainKey = (k) => (/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(String(k)) ? JSON.stringify(k) : "that is not a plain name");
// A binding (and seat) name. Defined here, not in binding.mjs, so journal.mjs can label a
// journal directory without importing binding.mjs and its config readers; copies of
// journal.mjs + errors.mjs (the queue test repo) stay self-contained. binding.mjs re-exports it.
export const BINDING_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
// A filesystem call that fails names the file by label, the call and the
// error code, never the path Node puts in the message. The same error object
// is rethrown, so its type, code and the exit it maps to are unchanged, and
// it is marked so the CLI's top-level handler prints it as it is (#1873).
const QUIET = Symbol("discord.quiet");
const SYSCALL = /^[a-z][a-z0-9_]{0,31}$/;
const CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
const callOf = (e) => {
const word = typeof e.syscall === "string" ? e.syscall.split(" ")[0] : "";
return SYSCALL.test(word) ? word : "access";
};
const codeOf = (e) => (typeof e.code === "string" && CODE.test(e.code) ? e.code : "UNKNOWN");
export function quietFs(e, label, verb = null) {
if (e && typeof e === "object" && typeof e.code === "string") {
e.message = `cannot ${verb ?? callOf(e)} ${label} (${codeOf(e)})`;
e[QUIET] = true;
}
return e;
}
export const quietRead = (e, label) => quietFs(e, label, "read");
// What a top-level handler prints for an error (#1873). Our refusals and an
// error already labelled at its source print their own message. A native
// Node error (it carries a syscall, or a code and a path) that slipped past
// every source label prints only its call and code, never its message,
// which carries the path; `label` names the file when the error has one.
export function printable(err, label = "a discord file") {
if (err instanceof DiscordError || (err && err[QUIET] === true)) return String(err.message);
if (err && typeof err === "object" && (typeof err.syscall === "string" || (typeof err.code === "string" && typeof err.path === "string"))) {
return typeof err.path === "string" ? `cannot ${callOf(err)} ${label} (${codeOf(err)})` : `cannot ${callOf(err)} (${codeOf(err)})`;
}
return err && typeof err.message === "string" ? err.message : String(err);
}
export class DiscordError extends Error {
constructor(message, exitCode = 2, details = undefined) {
super(message);
this.name = "DiscordError";
this.exitCode = exitCode;
if (details !== undefined) this.details = details;
}
}