/**
 * Safe-ID validation — reject any ID before it reaches a filesystem path
 * or DB write where path-traversal characters could escape `.sf/`.
 *
 * sf tool handlers historically only checked "non-empty string" on
 * milestoneId / sliceId / taskId fields and then interpolated those into
 * paths like `milestones/${id}/slices/${id}/...`. That allowed inputs
 * such as `../outside`, `S01/../../x`, or `T01\nbad: true` to either
 * write outside `.sf` or corrupt downstream YAML/JSON consumers.
 *
 * This module provides a single chokepoint validator. It is intentionally
 * focused on SECURITY (path traversal, separators, control chars) rather
 * than format. Format-specific patterns (e.g. MILESTONE_ID_RE) live next
 * to their domain in `milestone-id-utils.ts` etc.
 *
 * Filed under `validation-safe-id-path-segments` in the self-feedback
 * backlog. Apply at every tool-handler entry point that uses an ID in a
 * filesystem path or DB write.
 */
const FORBIDDEN_CHAR = /[\\/\x00-\x1f\n\r\t]/;
const MAX_ID_LENGTH = 64;
export class UnsafeIdError extends TypeError {
	constructor(fieldName, reason, value) {
		super(
			`${fieldName} is unsafe: ${reason} (got ${JSON.stringify(value).slice(0, 80)})`,
		);
		this.fieldName = fieldName;
		this.reason = reason;
		this.value = value;
		this.name = "UnsafeIdError";
	}
	fieldName;
	reason;
	value;
}
/**
 * Validate a single string is safe to use as a path segment. Throws
 * UnsafeIdError on rejection so callers get a structured error they can
 * surface to the agent without leaking the full attempted value into logs.
 */
export function validateSafePathSegment(value, fieldName) {
	if (typeof value !== "string") {
		throw new UnsafeIdError(fieldName, "not a string", String(value));
	}
	if (value.length === 0) {
		throw new UnsafeIdError(fieldName, "empty", value);
	}
	if (value.length > MAX_ID_LENGTH) {
		throw new UnsafeIdError(
			fieldName,
			`exceeds ${MAX_ID_LENGTH} characters`,
			value,
		);
	}
	if (value === "." || value === "..") {
		throw new UnsafeIdError(fieldName, ". or ..", value);
	}
	if (FORBIDDEN_CHAR.test(value)) {
		throw new UnsafeIdError(
			fieldName,
			"contains path separator, control char, newline, tab, or NUL",
			value,
		);
	}
	if (value.includes("..")) {
		throw new UnsafeIdError(fieldName, "contains '..' (path traversal)", value);
	}
	if (value.startsWith(".") || value.startsWith("-")) {
		throw new UnsafeIdError(fieldName, "starts with '.' or '-'", value);
	}
}
/**
 * Validate the standard milestone/slice/task ID trio. Any field that is
 * undefined or null is skipped; any field that is present is validated.
 * Throws UnsafeIdError on the first failure.
 */
export function assertSafeIds(input) {
	if (input.milestoneId != null)
		validateSafePathSegment(input.milestoneId, "milestone_id");
	if (input.sliceId != null) validateSafePathSegment(input.sliceId, "slice_id");
	if (input.taskId != null) validateSafePathSegment(input.taskId, "task_id");
}
/**
 * Convenience: returns the first UnsafeIdError encountered or null if all
 * provided IDs are safe. Use when the caller wants to handle the error
 * inline rather than via try/catch.
 */
export function checkSafeIds(input) {
	try {
		assertSafeIds(input);
		return null;
	} catch (err) {
		if (err instanceof UnsafeIdError) return err;
		throw err;
	}
}
