{"record":{"id":"f66da0dd03240cf7","repo":"santifer/career-ops","slug":"refusing-to-write-the-cover-letter-outside-output","errorCode":null,"errorMessage":"Refusing to write the cover letter outside output/: (empty path)","messagePattern":"Refusing to write the cover letter outside output/: \\(empty path\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"generate-cover-letter.mjs","lineNumber":40,"sourceCode":"import { resolveTemplate } from \"./cv-templates.mjs\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst OUTPUT_ROOT = resolve(__dirname, \"output\");\n\n/**\n * Resolve a requested cover-letter output path.\n *\n * Paths that stay inside `output/` keep their relative subdirectory (the\n * application-bundle layout `generate-pdf.mjs` already supports). Paths that\n * would escape `output/` — `..` traversal or an absolute path outside it —\n * are rejected instead of being silently flattened to `output/<basename>`.\n *\n * @param {string} raw - Caller-supplied --out / payload.output_path value.\n * @returns {string} Absolute path inside OUTPUT_ROOT.\n */\nexport function safeOutputPath(raw) {\n  if (raw == null || String(raw).trim() === \"\") {\n    throw new Error(\"Refusing to write the cover letter outside output/: (empty path)\");\n  }\n  const trimmed = String(raw).trim();\n\n  const asWritten = resolve(trimmed);\n  if (containedInOutput(asWritten)) return asWritten;\n\n  // Absolute paths and any `..` segment already chose a location; if that\n  // location is not inside output/, refuse instead of rewriting to a basename.\n  if (isAbsolute(trimmed) || /(^|[\\\\/])\\.\\.([\\\\/]|$)/.test(trimmed)) {\n    throw new Error(`Refusing to write the cover letter outside output/: ${raw}`);\n  }\n\n  // Bare filename or a relative path that is not already under output/\n  // (e.g. --out cover.pdf, or --out output/foo/bar.pdf from another cwd).\n  const posix = trimmed.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n  const relativeToRoot = posix === \"output\" || posix === \"output/\"\n    ? \"\"\n    : posix.startsWith(\"output/\")","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/santifer/career-ops/blob/60398d6549a46f5266929538af21cfab94badc75/generate-cover-letter.mjs#L22-L58","documentation":"safeOutputPath() in generate-cover-letter.mjs is the first guard on the --out / payload.output_path value: null, empty, or whitespace-only input is rejected immediately because an empty destination is meaningless and would otherwise normalize to the output/ directory itself rather than a letter file.","triggerScenarios":"Calling the script with `--out \"\"` or `--out \"   \"`; a payload JSON where output_path is absent and caller code forwards `payload.output_path` unconditionally (undefined); shell script passing an unset variable: `--out \"$OUT_PATH\"` with OUT_PATH empty.","commonSituations":"Automation/glue code that builds the out path from optional job fields (company or role slug empty); env var not exported in a wrapper script; copy-pasted command with a placeholder never filled in.","solutions":["Pass a real filename: `--out acme-cover.pdf` (anchored under output/) or `--out output/acme/cover.pdf`.","If the path is computed, default it before invoking: `--out \"${SLUG:-cover}.pdf\"`.","Fix the payload generator to either set output_path or omit the flag entirely."],"exampleFix":"# before\nOUT_PATH=\"\"\nnode generate-cover-letter.mjs --payload p.json --out \"$OUT_PATH\"\n\n# after\nOUT_PATH=\"acme-cover.pdf\"\nnode generate-cover-letter.mjs --payload p.json --out \"$OUT_PATH\"","handlingStrategy":"validation","validationCode":"const raw = args.out ?? payload.output_path;\nif (raw == null || String(raw).trim() === '') {\n  console.error('Provide a cover-letter filename, e.g. --out acme-cover.pdf');\n  process.exit(1);\n}\nconst outPath = safeOutputPath(raw);","typeGuard":"/** @param {unknown} v */\nfunction isNonEmptyPath(v) {\n  return typeof v === 'string' && v.trim() !== '' && v.trim() !== '.';\n}","tryCatchPattern":null,"preventionTips":["Default computed filenames in the calling script: `--out \"${SLUG:-cover}.pdf\"`.","Treat an unset output_path in a payload as 'let the tool choose', i.e. omit the flag, never forward undefined.","Fail fast in wrappers when a required variable is empty (`: \"${OUT:?set OUT}\"` in bash)."],"tags":["cli","path-validation","empty-argument"],"backgroundTag":"missing-required-argument","analyzedSha":"60398d6549a46f5266929538af21cfab94badc75","analyzedAt":"2026-08-20T23:00:06.764Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}