import { randomUUID } from 'node:crypto';
import fs from 'node:fs';
import { access, lstat, mkdir, readFile, readdir, readlink, realpath, stat, writeFile, } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import { parseFrontMatter } from '../shared/frontmatter.js';
// ---------------------------
//----------------- HTTP HANDLER UTILITIES ------------
/**
 * Wraps arbitrary data in the standard API success envelope.
 *
 * Use this helper in route handlers to keep successful JSON responses consistent
 * across endpoints.
 */
export function createApiSuccessResponse(data) {
    return {
        success: true,
        data,
    };
}
/**
 * Converts an async Express handler into a standard `RequestHandler` and routes
 * rejected promises to Express error middleware.
 *
 * Use this to avoid repeating `try/catch(next)` in every async route.
 */
export function asyncHandler(handler) {
    return (req, res, next) => {
        void Promise.resolve(handler(req, res, next)).catch(next);
    };
}
// ---------------------------
//----------------- SHARED ERROR UTILITIES ------------
/**
 * Shared application error with HTTP status and machine-readable code metadata.
 *
 * Throw this from service/route layers when the caller should receive a
 * controlled error response rather than a generic 500.
 */
export class AppError extends Error {
    code;
    statusCode;
    details;
    constructor(message, options = {}) {
        super(message);
        this.name = 'AppError';
        this.code = options.code ?? 'INTERNAL_ERROR';
        this.statusCode = options.statusCode ?? 500;
        this.details = options.details;
    }
}
// ---------------------------
//----------------- WORKSPACE PATH VALIDATION UTILITIES ------------
/**
 * Shared base directory under which every website user gets their own
 * subdirectory. The per-user root is always `<WORKSPACES_ROOT>/<username>` —
 * even when an operator sets `WORKSPACES_ROOT` explicitly — so that two
 * website users can never browse/create projects in each other's space.
 *
 * Default is `<homedir>/web-code-workspaces` so the app keeps its files in a
 * dedicated directory instead of polluting the host user's home.
 */
export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || path.join(os.homedir(), 'web-code-workspaces');
/**
 * Strips characters that are unsafe in directory names so a malicious or
 * unusual username cannot escape its workspace sandbox or collide with
 * sibling directories. Returns null if the result is empty/`.`/`..`.
 */
export function sanitizeUsernameForPath(username) {
    if (typeof username !== 'string') {
        return null;
    }
    const trimmed = username.trim();
    if (!trimmed) {
        return null;
    }
    const sanitized = trimmed.replace(/[^A-Za-z0-9._-]/g, '_');
    if (!sanitized || sanitized === '.' || sanitized === '..') {
        return null;
    }
    return sanitized;
}
/**
 * Returns the absolute per-user workspace root for `username`. Does not touch
 * the filesystem; callers that need the directory to exist before use should
 * call `ensureUserWorkspaceRoot` instead.
 */
export function getUserWorkspaceRoot(username) {
    const safe = sanitizeUsernameForPath(username);
    if (!safe) {
        throw new AppError('Authenticated user is required', {
            code: 'AUTHENTICATION_REQUIRED',
            statusCode: 401,
        });
    }
    return path.join(WORKSPACES_ROOT, safe);
}
/**
 * Ensures the per-user workspace directory exists (mkdir -p) and returns its
 * absolute path. Safe to call on every request — `recursive: true` makes it
 * idempotent.
 */
export async function ensureUserWorkspaceRoot(username) {
    const userRoot = getUserWorkspaceRoot(username);
    await mkdir(userRoot, { recursive: true });
    return userRoot;
}
/**
 * System-critical paths that must never be used as workspace roots.
 *
 * The validation helper blocks these values directly and also blocks paths
 * nested under them (with explicit allow-list exceptions where necessary).
 */
export const FORBIDDEN_WORKSPACE_PATHS = [
    // Unix
    '/',
    '/etc',
    '/bin',
    '/sbin',
    '/usr',
    '/dev',
    '/proc',
    '/sys',
    '/var',
    '/boot',
    '/root',
    '/lib',
    '/lib64',
    '/opt',
    '/tmp',
    '/run',
    // Windows
    'C:\\Windows',
    'C:\\Program Files',
    'C:\\Program Files (x86)',
    'C:\\ProgramData',
    'C:\\System Volume Information',
    'C:\\$Recycle.Bin',
];
function stripWindowsLongPathPrefix(inputPath) {
    if (inputPath.startsWith('\\\\?\\UNC\\')) {
        return `\\\\${inputPath.slice('\\\\?\\UNC\\'.length)}`;
    }
    if (inputPath.startsWith('\\\\?\\')) {
        return inputPath.slice('\\\\?\\'.length);
    }
    return inputPath;
}
function shouldUseWindowsPathNormalization(inputPath) {
    if (process.platform === 'win32') {
        return true;
    }
    return inputPath.startsWith('\\\\') || /^[a-zA-Z]:([\\/]|$)/.test(inputPath);
}
/**
 * Canonicalizes project/workspace paths for stable DB keys and comparisons.
 *
 * Normalization rules:
 * - trim whitespace
 * - strip Windows long-path prefixes (`\\?\` and `\\?\UNC\`)
 * - normalize path separators and dot segments
 * - trim trailing separators except for filesystem roots
 */
export function normalizeProjectPath(inputPath) {
    if (typeof inputPath !== 'string') {
        return '';
    }
    const trimmed = inputPath.trim();
    if (!trimmed) {
        return '';
    }
    const withoutLongPrefix = stripWindowsLongPathPrefix(trimmed);
    const useWindowsPathRules = shouldUseWindowsPathNormalization(withoutLongPrefix);
    const normalized = useWindowsPathRules
        ? path.win32.normalize(withoutLongPrefix)
        : path.posix.normalize(withoutLongPrefix);
    if (!normalized) {
        return '';
    }
    const parser = useWindowsPathRules ? path.win32 : path.posix;
    const root = parser.parse(normalized).root;
    if (normalized === root) {
        return normalized;
    }
    return normalized.replace(/[\\/]+$/, '');
}
/**
 * Validates that a user-supplied workspace path is safe to use.
 *
 * Call this before any filesystem mutation that creates or registers projects.
 * The function resolves symlinks, enforces containment within `workspaceRoot`
 * (defaults to the app-wide `WORKSPACES_ROOT`), and blocks known system
 * directories. Pass the per-user root for multi-user isolation.
 */
export async function validateWorkspacePath(requestedPath, workspaceRoot = WORKSPACES_ROOT) {
    try {
        const normalizedRequestedPath = normalizeProjectPath(requestedPath);
        if (!normalizedRequestedPath) {
            return {
                valid: false,
                error: 'Workspace path is required',
            };
        }
        const absolutePath = path.resolve(normalizedRequestedPath);
        const normalizedPath = normalizeProjectPath(absolutePath);
        if (FORBIDDEN_WORKSPACE_PATHS.includes(normalizedPath) || normalizedPath === '/') {
            return {
                valid: false,
                error: 'Cannot use system-critical directories as workspace locations',
            };
        }
        for (const forbiddenPath of FORBIDDEN_WORKSPACE_PATHS) {
            const normalizedForbiddenPath = normalizeProjectPath(forbiddenPath);
            if (normalizedPath === normalizedForbiddenPath
                || normalizedPath.startsWith(`${normalizedForbiddenPath}${path.sep}`)) {
                // Allow specific user-writable folders under /var.
                if (normalizedForbiddenPath === '/var'
                    && (normalizedPath.startsWith('/var/tmp') || normalizedPath.startsWith('/var/folders'))) {
                    continue;
                }
                return {
                    valid: false,
                    error: `Cannot create workspace in system directory: ${forbiddenPath}`,
                };
            }
        }
        let resolvedPath = normalizeProjectPath(absolutePath);
        try {
            await access(absolutePath);
            resolvedPath = normalizeProjectPath(await realpath(absolutePath));
        }
        catch (error) {
            const fileError = error;
            if (fileError.code !== 'ENOENT') {
                throw fileError;
            }
            const parentPath = path.dirname(absolutePath);
            try {
                const parentRealPath = await realpath(parentPath);
                resolvedPath = normalizeProjectPath(path.join(parentRealPath, path.basename(absolutePath)));
            }
            catch (parentError) {
                const parentFileError = parentError;
                if (parentFileError.code !== 'ENOENT') {
                    throw parentFileError;
                }
            }
        }
        // Ensure the workspace root exists before realpath so we can give a clean
        // error to admins who haven't created it yet. Cheap & idempotent.
        await mkdir(workspaceRoot, { recursive: true });
        const resolvedWorkspaceRoot = normalizeProjectPath(await realpath(workspaceRoot));
        if (!resolvedPath.startsWith(`${resolvedWorkspaceRoot}${path.sep}`)
            && resolvedPath !== resolvedWorkspaceRoot) {
            return {
                valid: false,
                error: `Workspace path must be within the allowed workspace root: ${workspaceRoot}`,
            };
        }
        try {
            await access(absolutePath);
            const pathStats = await lstat(absolutePath);
            if (pathStats.isSymbolicLink()) {
                const symlinkTarget = await readlink(absolutePath);
                const resolvedSymlinkPath = path.resolve(path.dirname(absolutePath), symlinkTarget);
                const realSymlinkPath = await realpath(resolvedSymlinkPath);
                if (!realSymlinkPath.startsWith(`${resolvedWorkspaceRoot}${path.sep}`)
                    && realSymlinkPath !== resolvedWorkspaceRoot) {
                    return {
                        valid: false,
                        error: 'Symlink target is outside the allowed workspace root',
                    };
                }
            }
        }
        catch (error) {
            const fileError = error;
            if (fileError.code !== 'ENOENT') {
                throw fileError;
            }
        }
        return {
            valid: true,
            resolvedPath,
        };
    }
    catch (error) {
        return {
            valid: false,
            error: `Path validation failed: ${error.message}`,
        };
    }
}
// ---------------------------
//----------------- NORMALIZED PROVIDER MESSAGE UTILITIES ------------
/**
 * Generates a stable unique id for normalized provider messages.
 */
export function generateMessageId(prefix = 'msg') {
    return `${prefix}_${randomUUID()}`;
}
/**
 * Creates a normalized provider message and fills the shared envelope fields.
 *
 * Provider adapters and live SDK handlers pass through provider-specific fields,
 * while this helper guarantees every emitted event has an id, session id,
 * timestamp, and provider marker.
 */
export function createNormalizedMessage(fields) {
    return {
        ...fields,
        id: fields.id || generateMessageId(fields.kind),
        sessionId: fields.sessionId || '',
        timestamp: fields.timestamp || new Date().toISOString(),
        provider: fields.provider,
    };
}
// ---------------------------
//----------------- MCP CONFIG PARSING UTILITIES ------------
/**
 * Safely narrows an unknown value to a plain object record.
 *
 * This deliberately rejects arrays, `null`, and primitive values so callers can
 * treat the returned value as a JSON-style object map without repeating the same
 * defensive shape checks at every config read site.
 */
export const readObjectRecord = (value) => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        return null;
    }
    return value;
};
/**
 * Reads an optional string from unknown input and normalizes empty or whitespace-only
 * values to `undefined`.
 *
 * This is useful when parsing config files where a field may be missing, present
 * with the wrong type, or present as an empty string that should be treated as
 * "not configured".
 */
export const readOptionalString = (value) => {
    if (typeof value !== 'string') {
        return undefined;
    }
    const normalized = value.trim();
    return normalized.length > 0 ? normalized : undefined;
};
/**
 * Reads an optional string array from unknown input.
 *
 * Non-array values are ignored, and any array entries that are not strings are
 * filtered out. This lets provider config readers consume loosely shaped JSON/TOML
 * data without failing on incidental invalid members.
 */
export const readStringArray = (value) => {
    if (!Array.isArray(value)) {
        return undefined;
    }
    return value.filter((entry) => typeof entry === 'string');
};
/**
 * Reads an optional string-to-string map from unknown input.
 *
 * The function first ensures the source value is a plain object, then keeps only
 * keys whose values are strings. If no valid entries remain, it returns `undefined`
 * so callers can distinguish "no usable map" from an empty object that was
 * intentionally authored downstream.
 */
export const readStringRecord = (value) => {
    const record = readObjectRecord(value);
    if (!record) {
        return undefined;
    }
    const normalized = {};
    for (const [key, entry] of Object.entries(record)) {
        if (typeof entry === 'string') {
            normalized[key] = entry;
        }
    }
    return Object.keys(normalized).length > 0 ? normalized : undefined;
};
// ---------------------------
//----------------- WEBSOCKET PAYLOAD PARSING UTILITIES ------------
/**
 * Parses one websocket message payload into a plain JSON object record.
 *
 * Use this in realtime handlers that receive raw websocket payloads as `string`,
 * `Buffer`, `ArrayBuffer`, or chunk arrays. The helper converts supported
 * payload formats to UTF-8 text, parses JSON, and returns only object payloads.
 * Primitive/array/invalid payloads return `null` so callers can handle bad input
 * without throwing from deeply nested message handlers.
 */
export const parseIncomingJsonObject = (payload) => {
    let text = null;
    if (typeof payload === 'string') {
        text = payload;
    }
    else if (Buffer.isBuffer(payload)) {
        text = payload.toString('utf8');
    }
    else if (payload instanceof ArrayBuffer) {
        text = Buffer.from(payload).toString('utf8');
    }
    else if (Array.isArray(payload)) {
        const buffers = payload
            .map((entry) => {
            if (Buffer.isBuffer(entry)) {
                return entry;
            }
            if (entry instanceof ArrayBuffer) {
                return Buffer.from(entry);
            }
            if (ArrayBuffer.isView(entry)) {
                return Buffer.from(entry.buffer, entry.byteOffset, entry.byteLength);
            }
            return null;
        })
            .filter((entry) => entry !== null);
        if (buffers.length > 0) {
            text = Buffer.concat(buffers).toString('utf8');
        }
    }
    if (typeof text !== 'string' || text.trim().length === 0) {
        return null;
    }
    try {
        const parsed = JSON.parse(text);
        return readObjectRecord(parsed);
    }
    catch {
        return null;
    }
};
/**
 * Reads a JSON config file and guarantees a plain object result.
 *
 * Missing files are treated as an empty config object so provider-specific MCP
 * readers can operate against first-run environments without special-case file
 * existence checks. If the file exists but contains invalid JSON, the parse error
 * is preserved and rethrown.
 */
export const readJsonConfig = async (filePath) => {
    try {
        const content = await readFile(filePath, 'utf8');
        const parsed = JSON.parse(content);
        return readObjectRecord(parsed) ?? {};
    }
    catch (error) {
        const code = error.code;
        if (code === 'ENOENT') {
            return {};
        }
        throw error;
    }
};
/**
 * Writes a JSON config file with stable, human-readable formatting.
 *
 * The parent directory is created automatically so callers can persist config into
 * provider-specific folders without pre-creating the directory tree. Output always
 * ends with a trailing newline to keep the file diff-friendly.
 */
export const writeJsonConfig = async (filePath, data) => {
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
};
// ---------------------------
//----------------- PROVIDER SKILL FILE UTILITIES ------------
/**
 * Finds direct child skill markdown files under a provider skill root.
 *
 * Skill systems usually store one skill per child directory, so direct mode
 * scans only `<root>/<skill-name>/SKILL.md`. Recursive mode is reserved for
 * provider sources that can nest skills arbitrarily, and it returns every
 * descendant `SKILL.md`. Missing or unreadable roots return an empty list
 * because users may not have every provider installed or configured.
 */
export async function findProviderSkillMarkdownFiles(rootDir, options = {}) {
    const skillFiles = [];
    const collectRecursive = async (dirPath) => {
        let entries;
        try {
            entries = await readdir(dirPath, { withFileTypes: true });
        }
        catch {
            return;
        }
        try {
            const skillPath = path.join(dirPath, 'SKILL.md');
            const skillStats = await stat(skillPath);
            if (skillStats.isFile()) {
                skillFiles.push(skillPath);
            }
        }
        catch {
            // Directories without SKILL.md are expected while walking plugin trees.
        }
        for (const entry of entries) {
            if (entry.isDirectory()) {
                await collectRecursive(path.join(dirPath, entry.name));
            }
        }
    };
    if (options.recursive) {
        await collectRecursive(rootDir);
        return skillFiles.sort((left, right) => left.localeCompare(right));
    }
    try {
        const entries = await readdir(rootDir, { withFileTypes: true });
        for (const entry of entries) {
            if (!entry.isDirectory()) {
                continue;
            }
            const skillPath = path.join(rootDir, entry.name, 'SKILL.md');
            try {
                const skillStats = await stat(skillPath);
                if (skillStats.isFile()) {
                    skillFiles.push(skillPath);
                }
            }
            catch {
                // A partial skill directory should not block discovery of sibling skills.
            }
        }
        return skillFiles.sort((left, right) => left.localeCompare(right));
    }
    catch {
        return [];
    }
}
/**
 * Reads the `name` and `description` fields from a provider skill markdown file.
 *
 * The metadata is expected in markdown front matter. If a skill omits `name`, the
 * parent directory name is used as a stable fallback so providers can still
 * expose the skill. Missing descriptions are normalized to an empty string.
 */
export async function readProviderSkillMarkdownDefinition(skillPath) {
    const content = await readFile(skillPath, 'utf8');
    const parsed = parseFrontMatter(content);
    const data = readObjectRecord(parsed.data) ?? {};
    const fallbackName = path.basename(path.dirname(skillPath));
    return {
        name: readOptionalString(data.name) ?? fallbackName,
        description: readOptionalString(data.description) ?? '',
    };
}
// ---------------------------
//----------------- SESSION SYNCHRONIZER TITLE HELPERS ------------
/**
 * Produces a compact session title suitable for UI rendering and DB storage.
 *
 * Use this when converting provider-native names into a consistent title value.
 * The helper collapses repeated whitespace, trims the result, and truncates it
 * to 120 characters so every provider writes stable and bounded metadata.
 * If the normalized input is empty, it returns the supplied fallback title.
 */
export function normalizeSessionName(rawValue, fallback) {
    const normalized = (rawValue ?? '').replace(/\s+/g, ' ').trim();
    if (!normalized) {
        return fallback;
    }
    return normalized.slice(0, 120);
}
// ---------------------------
//----------------- SESSION SYNCHRONIZER FILESYSTEM HELPERS ------------
/**
 * Recursively discovers files that match one extension, with optional incremental filtering.
 *
 * Provider synchronizers call this to find transcript artifacts under provider
 * home directories. Pass `lastScanAt` to include only files created after the
 * previous scan, or pass `null` to perform a full rescan. Missing directories
 * are treated as empty because not every provider exists on every machine.
 */
export async function findFilesRecursivelyCreatedAfter(rootDir, extension, lastScanAt, fileList = []) {
    try {
        const entries = await readdir(rootDir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(rootDir, entry.name);
            if (entry.isDirectory()) {
                await findFilesRecursivelyCreatedAfter(fullPath, extension, lastScanAt, fileList);
                continue;
            }
            if (!entry.isFile() || !entry.name.endsWith(extension)) {
                continue;
            }
            if (!lastScanAt) {
                fileList.push(fullPath);
                continue;
            }
            const fileStat = await stat(fullPath);
            if (fileStat.birthtime > lastScanAt) {
                fileList.push(fullPath);
            }
        }
    }
    catch {
        // Missing provider folders are expected in first-run or partial setups.
    }
    return fileList;
}
/**
 * Reads file creation/update timestamps and maps them to DB-friendly ISO strings.
 *
 * Session indexers use this to persist `created_at` and `updated_at` metadata
 * when upserting sessions. If the file cannot be read, an empty object is
 * returned so indexing can continue for other files.
 */
export async function readFileTimestamps(filePath) {
    try {
        const fileStat = await stat(filePath);
        return {
            createdAt: fileStat.birthtime.toISOString(),
            updatedAt: fileStat.mtime.toISOString(),
        };
    }
    catch {
        return {};
    }
}
// ---------------------------
//----------------- SESSION SYNCHRONIZER JSONL PARSING HELPERS ------------
/**
 * Builds a first-seen key/value lookup map from a JSONL file.
 *
 * Use this for provider index files where session id -> display name metadata
 * is stored line-by-line. The first value for each key wins, preserving the
 * earliest known label while avoiding repeated map overwrites.
 */
export async function buildLookupMap(filePath, keyField, valueField) {
    const lookup = new Map();
    try {
        const fileStream = fs.createReadStream(filePath);
        const lineReader = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
        for await (const line of lineReader) {
            const trimmed = line.trim();
            if (!trimmed) {
                continue;
            }
            const parsed = JSON.parse(trimmed);
            const key = parsed[keyField];
            const value = parsed[valueField];
            if (typeof key === 'string' && typeof value === 'string' && !lookup.has(key)) {
                lookup.set(key, value);
            }
        }
    }
    catch {
        // Missing or unreadable lookup files should not block session sync.
    }
    return lookup;
}
/**
 * Reads a JSONL file and returns the first extracted payload that matches caller criteria.
 *
 * The caller supplies an `extractor` that validates provider-specific row
 * shapes. This helper centralizes line-by-line parsing and lets indexers stop
 * scanning as soon as one valid row is found.
 */
export async function extractFirstValidJsonlData(filePath, extractor) {
    try {
        const fileStream = fs.createReadStream(filePath);
        const lineReader = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
        for await (const line of lineReader) {
            const trimmed = line.trim();
            if (!trimmed) {
                continue;
            }
            const parsed = JSON.parse(trimmed);
            const extracted = extractor(parsed);
            if (extracted) {
                lineReader.close();
                fileStream.close();
                return extracted;
            }
        }
    }
    catch {
        // Ignore malformed or missing artifacts so full scans keep progressing.
    }
    return null;
}
//# sourceMappingURL=utils.js.map