251148c3ff
## Summary - add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers - reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities - clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance ## Notable changes - docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs - new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement - refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code - changelog, development documentation, and CI surface updates ## Test Plan - [ ] `pnpm run check` - [ ] review generated/manifests and skill sync outputs - [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs ## Notes - this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com> Reviewed-on: #1
78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
/**
|
|
* safe-replace-dir.mjs — safely replace a directory within a safety-root boundary
|
|
*
|
|
* Exports:
|
|
* safeReplaceDir(source, target, safetyRoot) → Promise<void>
|
|
*
|
|
* Usage:
|
|
* import { safeReplaceDir } from "./lib/safe-replace-dir.mjs";
|
|
* await safeReplaceDir("/path/to/source", "/safe/root/target", "/safe/root");
|
|
*
|
|
* Safety contract:
|
|
* - `target` must be a strict descendant of `safetyRoot` (not equal to it).
|
|
* - `target` must be a non-empty path.
|
|
* - Throws with a descriptive message if either constraint is violated.
|
|
*
|
|
* Behaviour:
|
|
* - Removes any existing content at `target` (rm -rf equivalent).
|
|
* - Creates `target` (and any missing parent directories).
|
|
* - Copies all files from `source` into `target`.
|
|
*/
|
|
|
|
import { cp, mkdir, realpath, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* Safely replace `target` with the contents of `source`, enforcing that
|
|
* `target` is a strict descendant of `safetyRoot`.
|
|
*
|
|
* @param {string} source - Directory to copy from.
|
|
* @param {string} target - Directory to replace (will be removed then recreated).
|
|
* @param {string} safetyRoot - Ancestor boundary; `target` must be inside this.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function safeReplaceDir(source, target, safetyRoot) {
|
|
if (!target || target === "") {
|
|
throw new Error(`Refusing to replace unsafe target: (empty string)`);
|
|
}
|
|
|
|
const resolvedSafety = path.resolve(safetyRoot);
|
|
const resolvedTarget = path.resolve(target);
|
|
|
|
// Lexical check: target must be a strict descendant of safetyRoot.
|
|
const relative = path.relative(resolvedSafety, resolvedTarget);
|
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
|
|
throw new Error(`Refusing to replace target outside safety root: ${target}`);
|
|
}
|
|
|
|
// Real-path check: resolve the deepest existing ancestor of target's parent
|
|
// and verify it lies inside the real (symlink-resolved) safety root.
|
|
// This blocks a symlinked parent directory from redirecting outside the boundary.
|
|
const realSafety = await realpath(resolvedSafety);
|
|
let checkPath = path.dirname(resolvedTarget);
|
|
for (;;) {
|
|
try {
|
|
const realAncestor = await realpath(checkPath);
|
|
const realRel = path.relative(realSafety, realAncestor);
|
|
if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
|
|
throw new Error(`Refusing to replace target outside safety root: ${target}`);
|
|
}
|
|
break; // validation passed
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
const parent = path.dirname(checkPath);
|
|
if (parent === checkPath) {
|
|
throw new Error(`Refusing to replace target outside safety root: ${target}`, { cause: err });
|
|
}
|
|
checkPath = parent;
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
await rm(resolvedTarget, { recursive: true, force: true });
|
|
await mkdir(resolvedTarget, { recursive: true });
|
|
await cp(source, resolvedTarget, { recursive: true, force: true });
|
|
}
|