UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

196 lines 6.6 kB
import fs from "fs/promises"; import path from "path"; /** * Validates path for security (no .., no symlinks outside allowed dirs) * @param filePath Path to validate * @param basePath Base path for validation * @returns True if valid, throws otherwise */ export async function validateSecurePath(filePath, basePath) { // Reject path traversal if (filePath.includes("..")) { throw new Error(`Path traversal not allowed: ${filePath}`); } // Resolve real path (follows symlinks) let realPath; try { realPath = await fs.realpath(filePath); } catch (error) { if (error.code === "ENOENT") { // File doesn't exist yet - that's ok realPath = path.resolve(filePath); } else { throw error; } } // Verify symlink stays within allowed directories const resolvedBase = path.resolve(basePath); const oneBase = path.resolve(basePath, "one"); const isWithinInstallation = realPath.startsWith(resolvedBase); const isWithinOne = realPath.startsWith(oneBase); if (!isWithinInstallation && !isWithinOne) { throw new Error(`Symlink points outside allowed directories: ${realPath}`); } } /** * Checks if file exists * @param filePath Path to check * @returns True if exists, false otherwise */ export async function fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Gets group path by walking up the hierarchy * @param groupId Group ID * @param convexClient Optional Convex client for database queries * @returns Group path (e.g., "engineering/frontend") * * Note: This is a placeholder. In production, this should query Convex * to get the actual group hierarchy from the database. */ export async function getGroupPath(groupId, convexClient) { // TODO: Implement actual Convex query when backend is ready // For now, return the groupId as-is (flat structure) // // In production: // const group = await convexClient.query("groups:get", { id: groupId }); // const segments = [group.slug]; // let currentParentId = group.parentGroupId; // while (currentParentId) { // const parent = await convexClient.query("groups:get", { id: currentParentId }); // segments.unshift(parent.slug); // currentParentId = parent.parentGroupId; // } // return segments.join("/"); return groupId; // Fallback: flat structure } /** * Resolves a file hierarchically across group → installation → global * @param relativePath Relative path to file (e.g., "sprint-guide.md") * @param options File resolution options * @returns Resolved file with path and source */ export async function resolveFile(relativePath, options) { const { installationName, groupId, fallbackToGlobal = true, basePath = process.cwd(), } = options; // Validate relative path if (relativePath.includes("..")) { throw new Error(`Path traversal not allowed: ${relativePath}`); } const resolvedBase = path.resolve(basePath); // 1. If groupId provided, check group-specific paths (walk up hierarchy) if (groupId) { const groupPath = await getGroupPath(groupId); const groupFile = path.join(resolvedBase, installationName, "groups", groupPath, relativePath); if (await fileExists(groupFile)) { await validateSecurePath(groupFile, resolvedBase); return { path: groupFile, source: "group", exists: true, }; } // TODO: Walk up parent groups when backend supports it // For now, we only check the direct group path } // 2. Check installation root const installationFile = path.join(resolvedBase, installationName, relativePath); if (await fileExists(installationFile)) { await validateSecurePath(installationFile, resolvedBase); return { path: installationFile, source: "installation", exists: true, }; } // 3. Check global /one/ (if fallbackToGlobal) if (fallbackToGlobal) { const globalFile = path.join(resolvedBase, "one", relativePath); if (await fileExists(globalFile)) { await validateSecurePath(globalFile, resolvedBase); return { path: globalFile, source: "global", exists: true, }; } } // 4. Not found - return installation path as default (doesn't exist) return { path: installationFile, source: "installation", exists: false, }; } /** * Resolves multiple files at once * @param relativePaths Array of relative paths * @param options File resolution options * @returns Array of resolved files */ export async function resolveFiles(relativePaths, options) { return Promise.all(relativePaths.map((relativePath) => resolveFile(relativePath, options))); } /** * Loads file content with hierarchical resolution * @param relativePath Relative path to file * @param options File resolution options * @returns File content as string, or null if not found */ export async function loadFile(relativePath, options) { const resolved = await resolveFile(relativePath, options); if (!resolved.exists) { return null; } try { return await fs.readFile(resolved.path, "utf-8"); } catch (error) { console.error(`Error reading file ${resolved.path}:`, error); return null; } } /** * Cache for file resolution (optional performance optimization) */ export class FileResolverCache { constructor() { this.cache = new Map(); } /** * Gets cached result or resolves file * @param relativePath Relative path to file * @param options File resolution options * @returns Resolved file */ async resolve(relativePath, options) { const cacheKey = this.getCacheKey(relativePath, options); if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const resolved = await resolveFile(relativePath, options); this.cache.set(cacheKey, resolved); return resolved; } /** * Clears cache */ clear() { this.cache.clear(); } /** * Generates cache key */ getCacheKey(relativePath, options) { return `${options.installationName}:${options.groupId || "root"}:${relativePath}`; } } //# sourceMappingURL=file-resolver.js.map