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
JavaScript
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