mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
refactor: enhance security and streamline file handling
This commit introduces several improvements to the security and file handling mechanisms across the application. Key changes include: - Updated the Dockerfile to pin the GitHub CLI version for reproducible builds. - Refactored the secure file system operations to ensure consistent path validation and type handling. - Removed legacy path management functions and streamlined the allowed paths logic in the security module. - Enhanced route handlers to validate path parameters against the ALLOWED_ROOT_DIRECTORY, improving security against unauthorized access. - Updated the settings service to focus solely on the Anthropic API key, removing references to Google and OpenAI keys. These changes aim to enhance security, maintainability, and clarity in the codebase. Tests: All unit tests passing.
This commit is contained in:
@@ -26,13 +26,13 @@ RUN npm run build --workspace=apps/server
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install git, curl, and GitHub CLI
|
# Install git, curl, and GitHub CLI (pinned version for reproducible builds)
|
||||||
RUN apk add --no-cache git curl && \
|
RUN apk add --no-cache git curl && \
|
||||||
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \
|
GH_VERSION="2.63.2" && \
|
||||||
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
|
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
|
||||||
tar -xzf gh.tar.gz && \
|
tar -xzf gh.tar.gz && \
|
||||||
mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \
|
mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
|
||||||
rm -rf gh.tar.gz gh_*_linux_amd64
|
rm -rf gh.tar.gz "gh_${GH_VERSION}_linux_amd64"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
import type { Dirent } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { validatePath } from "./security.js";
|
import { validatePath } from "./security.js";
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export async function writeFile(
|
|||||||
encoding?: BufferEncoding
|
encoding?: BufferEncoding
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const validatedPath = validatePath(filePath);
|
const validatedPath = validatePath(filePath);
|
||||||
return fs.writeFile(validatedPath, data, encoding as any);
|
return fs.writeFile(validatedPath, data, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,12 +59,23 @@ export async function mkdir(
|
|||||||
/**
|
/**
|
||||||
* Wrapper around fs.readdir that validates path first
|
* Wrapper around fs.readdir that validates path first
|
||||||
*/
|
*/
|
||||||
|
export async function readdir(
|
||||||
|
dirPath: string,
|
||||||
|
options?: { withFileTypes?: false; encoding?: BufferEncoding }
|
||||||
|
): Promise<string[]>;
|
||||||
|
export async function readdir(
|
||||||
|
dirPath: string,
|
||||||
|
options: { withFileTypes: true; encoding?: BufferEncoding }
|
||||||
|
): Promise<Dirent[]>;
|
||||||
export async function readdir(
|
export async function readdir(
|
||||||
dirPath: string,
|
dirPath: string,
|
||||||
options?: { withFileTypes?: boolean; encoding?: BufferEncoding }
|
options?: { withFileTypes?: boolean; encoding?: BufferEncoding }
|
||||||
): Promise<string[] | any[]> {
|
): Promise<string[] | Dirent[]> {
|
||||||
const validatedPath = validatePath(dirPath);
|
const validatedPath = validatePath(dirPath);
|
||||||
return fs.readdir(validatedPath, options as any);
|
if (options?.withFileTypes === true) {
|
||||||
|
return fs.readdir(validatedPath, { withFileTypes: true });
|
||||||
|
}
|
||||||
|
return fs.readdir(validatedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,7 +127,7 @@ export async function appendFile(
|
|||||||
encoding?: BufferEncoding
|
encoding?: BufferEncoding
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const validatedPath = validatePath(filePath);
|
const validatedPath = validatePath(filePath);
|
||||||
return fs.appendFile(validatedPath, data, encoding as any);
|
return fs.appendFile(validatedPath, data, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ let allowedRootDirectory: string | null = null;
|
|||||||
// Data directory - always allowed for settings/credentials
|
// Data directory - always allowed for settings/credentials
|
||||||
let dataDirectory: string | null = null;
|
let dataDirectory: string | null = null;
|
||||||
|
|
||||||
// Allowed paths set - stores ALLOWED_ROOT_DIRECTORY and DATA_DIR
|
|
||||||
const allowedPaths = new Set<string>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize security settings from environment variables
|
* Initialize security settings from environment variables
|
||||||
* - ALLOWED_ROOT_DIRECTORY: main security boundary
|
* - ALLOWED_ROOT_DIRECTORY: main security boundary
|
||||||
@@ -36,7 +33,6 @@ export function initAllowedPaths(): void {
|
|||||||
const rootDir = process.env.ALLOWED_ROOT_DIRECTORY;
|
const rootDir = process.env.ALLOWED_ROOT_DIRECTORY;
|
||||||
if (rootDir) {
|
if (rootDir) {
|
||||||
allowedRootDirectory = path.resolve(rootDir);
|
allowedRootDirectory = path.resolve(rootDir);
|
||||||
allowedPaths.add(allowedRootDirectory);
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}`
|
`[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}`
|
||||||
);
|
);
|
||||||
@@ -50,19 +46,10 @@ export function initAllowedPaths(): void {
|
|||||||
const dataDir = process.env.DATA_DIR;
|
const dataDir = process.env.DATA_DIR;
|
||||||
if (dataDir) {
|
if (dataDir) {
|
||||||
dataDirectory = path.resolve(dataDir);
|
dataDirectory = path.resolve(dataDir);
|
||||||
allowedPaths.add(dataDirectory);
|
|
||||||
console.log(`[Security] ✓ DATA_DIR configured: ${dataDirectory}`);
|
console.log(`[Security] ✓ DATA_DIR configured: ${dataDirectory}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a path to the allowed list
|
|
||||||
* Used when dynamically creating new directories within the allowed root
|
|
||||||
*/
|
|
||||||
export function addAllowedPath(filePath: string): void {
|
|
||||||
allowedPaths.add(path.resolve(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is allowed based on ALLOWED_ROOT_DIRECTORY
|
* Check if a path is allowed based on ALLOWED_ROOT_DIRECTORY
|
||||||
* Returns true if:
|
* Returns true if:
|
||||||
@@ -145,5 +132,12 @@ export function getDataDirectory(): string | null {
|
|||||||
* Get list of allowed paths (for debugging)
|
* Get list of allowed paths (for debugging)
|
||||||
*/
|
*/
|
||||||
export function getAllowedPaths(): string[] {
|
export function getAllowedPaths(): string[] {
|
||||||
return Array.from(allowedPaths);
|
const paths: string[] = [];
|
||||||
|
if (allowedRootDirectory) {
|
||||||
|
paths.push(allowedRootDirectory);
|
||||||
|
}
|
||||||
|
if (dataDirectory) {
|
||||||
|
paths.push(dataDirectory);
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
|
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
|
||||||
router.post("/status", createStatusHandler(autoModeService));
|
router.post("/status", validatePathParams("projectPath?"), createStatusHandler(autoModeService));
|
||||||
router.post("/run-feature", validatePathParams("projectPath"), createRunFeatureHandler(autoModeService));
|
router.post("/run-feature", validatePathParams("projectPath"), createRunFeatureHandler(autoModeService));
|
||||||
router.post("/verify-feature", createVerifyFeatureHandler(autoModeService));
|
router.post("/verify-feature", validatePathParams("projectPath"), createVerifyFeatureHandler(autoModeService));
|
||||||
router.post("/resume-feature", createResumeFeatureHandler(autoModeService));
|
router.post("/resume-feature", validatePathParams("projectPath"), createResumeFeatureHandler(autoModeService));
|
||||||
router.post("/context-exists", createContextExistsHandler(autoModeService));
|
router.post("/context-exists", validatePathParams("projectPath"), createContextExistsHandler(autoModeService));
|
||||||
router.post("/analyze-project", validatePathParams("projectPath"), createAnalyzeProjectHandler(autoModeService));
|
router.post("/analyze-project", validatePathParams("projectPath"), createAnalyzeProjectHandler(autoModeService));
|
||||||
router.post(
|
router.post(
|
||||||
"/follow-up-feature",
|
"/follow-up-feature",
|
||||||
|
validatePathParams("projectPath", "imagePaths[]"),
|
||||||
createFollowUpFeatureHandler(autoModeService)
|
createFollowUpFeatureHandler(autoModeService)
|
||||||
);
|
);
|
||||||
router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService));
|
router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService));
|
||||||
router.post("/approve-plan", createApprovePlanHandler(autoModeService));
|
router.post("/approve-plan", validatePathParams("projectPath"), createApprovePlanHandler(autoModeService));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import type { Request, Response } from "express";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
|
import {
|
||||||
|
getAllowedRootDirectory,
|
||||||
|
isPathAllowed,
|
||||||
|
PathNotAllowedError,
|
||||||
|
} from "../../../lib/security.js";
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createBrowseHandler() {
|
export function createBrowseHandler() {
|
||||||
@@ -14,8 +18,9 @@ export function createBrowseHandler() {
|
|||||||
try {
|
try {
|
||||||
const { dirPath } = req.body as { dirPath?: string };
|
const { dirPath } = req.body as { dirPath?: string };
|
||||||
|
|
||||||
// Default to home directory if no path provided
|
// Default to ALLOWED_ROOT_DIRECTORY if set, otherwise home directory
|
||||||
const targetPath = dirPath ? path.resolve(dirPath) : os.homedir();
|
const defaultPath = getAllowedRootDirectory() || os.homedir();
|
||||||
|
const targetPath = dirPath ? path.resolve(dirPath) : defaultPath;
|
||||||
|
|
||||||
// Validate that the path is allowed
|
// Validate that the path is allowed
|
||||||
if (!isPathAllowed(targetPath)) {
|
if (!isPathAllowed(targetPath)) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath, isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
|
import { isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createMkdirHandler() {
|
export function createMkdirHandler() {
|
||||||
@@ -31,7 +31,6 @@ export function createMkdirHandler() {
|
|||||||
const stats = await fs.lstat(resolvedPath);
|
const stats = await fs.lstat(resolvedPath);
|
||||||
// Path exists - if it's a directory or symlink, consider it success
|
// Path exists - if it's a directory or symlink, consider it success
|
||||||
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
||||||
addAllowedPath(resolvedPath);
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,9 +51,6 @@ export function createMkdirHandler() {
|
|||||||
// Path doesn't exist, create it
|
// Path doesn't exist, create it
|
||||||
await fs.mkdir(resolvedPath, { recursive: true });
|
await fs.mkdir(resolvedPath, { recursive: true });
|
||||||
|
|
||||||
// Add the new directory to allowed paths for tracking
|
|
||||||
addAllowedPath(resolvedPath);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Path not allowed - return 403 Forbidden
|
// Path not allowed - return 403 Forbidden
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath } from "../../../lib/security.js";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createResolveDirectoryHandler() {
|
export function createResolveDirectoryHandler() {
|
||||||
@@ -30,7 +29,6 @@ export function createResolveDirectoryHandler() {
|
|||||||
const resolvedPath = path.resolve(directoryName);
|
const resolvedPath = path.resolve(directoryName);
|
||||||
const stats = await fs.stat(resolvedPath);
|
const stats = await fs.stat(resolvedPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
addAllowedPath(resolvedPath);
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
path: resolvedPath,
|
path: resolvedPath,
|
||||||
@@ -102,7 +100,6 @@ export function createResolveDirectoryHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Found matching directory
|
// Found matching directory
|
||||||
addAllowedPath(candidatePath);
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
path: candidatePath,
|
path: candidatePath,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath } from "../../../lib/security.js";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
import { getBoardDir } from "../../../lib/automaker-paths.js";
|
import { getBoardDir } from "../../../lib/automaker-paths.js";
|
||||||
|
|
||||||
@@ -43,9 +42,6 @@ export function createSaveBoardBackgroundHandler() {
|
|||||||
// Write file
|
// Write file
|
||||||
await fs.writeFile(filePath, buffer);
|
await fs.writeFile(filePath, buffer);
|
||||||
|
|
||||||
// Add board directory to allowed paths
|
|
||||||
addAllowedPath(boardDir);
|
|
||||||
|
|
||||||
// Return the absolute path
|
// Return the absolute path
|
||||||
res.json({ success: true, path: filePath });
|
res.json({ success: true, path: filePath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath } from "../../../lib/security.js";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
import { getImagesDir } from "../../../lib/automaker-paths.js";
|
import { getImagesDir } from "../../../lib/automaker-paths.js";
|
||||||
|
|
||||||
@@ -45,9 +44,6 @@ export function createSaveImageHandler() {
|
|||||||
// Write file
|
// Write file
|
||||||
await fs.writeFile(filePath, buffer);
|
await fs.writeFile(filePath, buffer);
|
||||||
|
|
||||||
// Add automaker directory to allowed paths
|
|
||||||
addAllowedPath(imagesDir);
|
|
||||||
|
|
||||||
// Return the absolute path
|
// Return the absolute path
|
||||||
res.json({ success: true, path: filePath });
|
res.json({ success: true, path: filePath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath, isPathAllowed } from "../../../lib/security.js";
|
import { isPathAllowed } from "../../../lib/security.js";
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createValidatePathHandler() {
|
export function createValidatePathHandler() {
|
||||||
@@ -31,9 +31,6 @@ export function createValidatePathHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to allowed paths
|
|
||||||
addAllowedPath(resolvedPath);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
path: resolvedPath,
|
path: resolvedPath,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Each provider shows: `{ configured: boolean, masked: string }`
|
* Each provider shows: `{ configured: boolean, masked: string }`
|
||||||
* Masked shows first 4 and last 4 characters for verification.
|
* Masked shows first 4 and last 4 characters for verification.
|
||||||
*
|
*
|
||||||
* Response: `{ "success": true, "credentials": { anthropic, google, openai } }`
|
* Response: `{ "success": true, "credentials": { anthropic } }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* PUT /api/settings/credentials - Update API credentials
|
* PUT /api/settings/credentials - Update API credentials
|
||||||
*
|
*
|
||||||
* Updates API keys for Anthropic, Google, or OpenAI. Partial updates supported.
|
* Updates API keys for Anthropic. Partial updates supported.
|
||||||
* Returns masked credentials for verification without exposing full keys.
|
* Returns masked credentials for verification without exposing full keys.
|
||||||
*
|
*
|
||||||
* Request body: `Partial<Credentials>` (usually just apiKeys)
|
* Request body: `Partial<Credentials>` (usually just apiKeys)
|
||||||
* Response: `{ "success": true, "credentials": { anthropic, google, openai } }`
|
* Response: `{ "success": true, "credentials": { anthropic } }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Request, Response } from "express";
|
|||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { addAllowedPath, isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
|
import { isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
|
||||||
import { logger, getErrorMessage, logError } from "../common.js";
|
import { logger, getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createCloneHandler() {
|
export function createCloneHandler() {
|
||||||
@@ -204,9 +204,6 @@ export function createCloneHandler() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to allowed paths
|
|
||||||
addAllowedPath(projectPath);
|
|
||||||
|
|
||||||
logger.info(`[Templates] Successfully cloned template to ${projectPath}`);
|
logger.info(`[Templates] Successfully cloned template to ${projectPath}`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { Request, Response } from "express";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
addAllowedPath,
|
|
||||||
getAllowedRootDirectory,
|
getAllowedRootDirectory,
|
||||||
getDataDirectory,
|
getDataDirectory,
|
||||||
} from "../../../lib/security.js";
|
} from "../../../lib/security.js";
|
||||||
@@ -41,9 +40,6 @@ export function createConfigHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add workspace dir to allowed paths
|
|
||||||
addAllowedPath(resolvedWorkspaceDir);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
configured: true,
|
configured: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { addAllowedPath, getAllowedRootDirectory } from "../../../lib/security.js";
|
import { getAllowedRootDirectory } from "../../../lib/security.js";
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
|
|
||||||
export function createDirectoriesHandler() {
|
export function createDirectoriesHandler() {
|
||||||
@@ -34,9 +34,6 @@ export function createDirectoriesHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add workspace dir to allowed paths
|
|
||||||
addAllowedPath(resolvedWorkspaceDir);
|
|
||||||
|
|
||||||
// Read directory contents
|
// Read directory contents
|
||||||
const entries = await fs.readdir(resolvedWorkspaceDir, { withFileTypes: true });
|
const entries = await fs.readdir(resolvedWorkspaceDir, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -49,9 +46,6 @@ export function createDirectoriesHandler() {
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Add each directory to allowed paths
|
|
||||||
directories.forEach((dir) => addAllowedPath(dir.path));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
directories,
|
directories,
|
||||||
|
|||||||
@@ -1117,7 +1117,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
// Check if directory exists first
|
// Check if directory exists first
|
||||||
await secureFs.access(contextDir);
|
await secureFs.access(contextDir);
|
||||||
|
|
||||||
const files = await secureFs.readdir(contextDir) as string[];
|
const files = await secureFs.readdir(contextDir);
|
||||||
// Filter for text-based context files (case-insensitive for Windows)
|
// Filter for text-based context files (case-insensitive for Windows)
|
||||||
const textFiles = files.filter((f) => {
|
const textFiles = files.filter((f) => {
|
||||||
const lower = f.toLowerCase();
|
const lower = f.toLowerCase();
|
||||||
@@ -1582,7 +1582,7 @@ Format your response as a structured markdown document.`;
|
|||||||
const featuresDir = getFeaturesDir(projectPath);
|
const featuresDir = getFeaturesDir(projectPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
|
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true });
|
||||||
const allFeatures: Feature[] = [];
|
const allFeatures: Feature[] = [];
|
||||||
const pendingFeatures: Feature[] = [];
|
const pendingFeatures: Feature[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -269,8 +269,6 @@ export class SettingsService {
|
|||||||
*/
|
*/
|
||||||
async getMaskedCredentials(): Promise<{
|
async getMaskedCredentials(): Promise<{
|
||||||
anthropic: { configured: boolean; masked: string };
|
anthropic: { configured: boolean; masked: string };
|
||||||
google: { configured: boolean; masked: string };
|
|
||||||
openai: { configured: boolean; masked: string };
|
|
||||||
}> {
|
}> {
|
||||||
const credentials = await this.getCredentials();
|
const credentials = await this.getCredentials();
|
||||||
|
|
||||||
@@ -284,14 +282,6 @@ export class SettingsService {
|
|||||||
configured: !!credentials.apiKeys.anthropic,
|
configured: !!credentials.apiKeys.anthropic,
|
||||||
masked: maskKey(credentials.apiKeys.anthropic),
|
masked: maskKey(credentials.apiKeys.anthropic),
|
||||||
},
|
},
|
||||||
google: {
|
|
||||||
configured: !!credentials.apiKeys.google,
|
|
||||||
masked: maskKey(credentials.apiKeys.google),
|
|
||||||
},
|
|
||||||
openai: {
|
|
||||||
configured: !!credentials.apiKeys.openai,
|
|
||||||
masked: maskKey(credentials.apiKeys.openai),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,14 +495,10 @@ export class SettingsService {
|
|||||||
if (appState.apiKeys) {
|
if (appState.apiKeys) {
|
||||||
const apiKeys = appState.apiKeys as {
|
const apiKeys = appState.apiKeys as {
|
||||||
anthropic?: string;
|
anthropic?: string;
|
||||||
google?: string;
|
|
||||||
openai?: string;
|
|
||||||
};
|
};
|
||||||
await this.updateCredentials({
|
await this.updateCredentials({
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: apiKeys.anthropic || "",
|
anthropic: apiKeys.anthropic || "",
|
||||||
google: apiKeys.google || "",
|
|
||||||
openai: apiKeys.openai || "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
migratedCredentials = true;
|
migratedCredentials = true;
|
||||||
|
|||||||
@@ -266,10 +266,6 @@ export interface Credentials {
|
|||||||
apiKeys: {
|
apiKeys: {
|
||||||
/** Anthropic Claude API key */
|
/** Anthropic Claude API key */
|
||||||
anthropic: string;
|
anthropic: string;
|
||||||
/** Google API key (for embeddings or other services) */
|
|
||||||
google: string;
|
|
||||||
/** OpenAI API key (for compatibility or alternative providers) */
|
|
||||||
openai: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,8 +406,6 @@ export const DEFAULT_CREDENTIALS: Credentials = {
|
|||||||
version: 1,
|
version: 1,
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "",
|
anthropic: "",
|
||||||
google: "",
|
|
||||||
openai: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,37 +51,6 @@ describe("security.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("addAllowedPath", () => {
|
|
||||||
it("should add path to allowed list", async () => {
|
|
||||||
delete process.env.ALLOWED_ROOT_DIRECTORY;
|
|
||||||
process.env.DATA_DIR = "";
|
|
||||||
|
|
||||||
const { initAllowedPaths, addAllowedPath, getAllowedPaths } =
|
|
||||||
await import("@/lib/security.js");
|
|
||||||
initAllowedPaths();
|
|
||||||
|
|
||||||
addAllowedPath("/new/path");
|
|
||||||
|
|
||||||
const allowed = getAllowedPaths();
|
|
||||||
expect(allowed).toContain(path.resolve("/new/path"));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve relative paths before adding", async () => {
|
|
||||||
delete process.env.ALLOWED_ROOT_DIRECTORY;
|
|
||||||
process.env.DATA_DIR = "";
|
|
||||||
|
|
||||||
const { initAllowedPaths, addAllowedPath, getAllowedPaths } =
|
|
||||||
await import("@/lib/security.js");
|
|
||||||
initAllowedPaths();
|
|
||||||
|
|
||||||
addAllowedPath("./relative/path");
|
|
||||||
|
|
||||||
const allowed = getAllowedPaths();
|
|
||||||
const cwd = process.cwd();
|
|
||||||
expect(allowed).toContain(path.resolve(cwd, "./relative/path"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isPathAllowed", () => {
|
describe("isPathAllowed", () => {
|
||||||
it("should allow paths within ALLOWED_ROOT_DIRECTORY", async () => {
|
it("should allow paths within ALLOWED_ROOT_DIRECTORY", async () => {
|
||||||
process.env.ALLOWED_ROOT_DIRECTORY = "/allowed/project";
|
process.env.ALLOWED_ROOT_DIRECTORY = "/allowed/project";
|
||||||
|
|||||||
@@ -183,8 +183,6 @@ describe("settings-service.ts", () => {
|
|||||||
...DEFAULT_CREDENTIALS,
|
...DEFAULT_CREDENTIALS,
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-test-key",
|
anthropic: "sk-test-key",
|
||||||
google: "",
|
|
||||||
openai: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const credentialsPath = path.join(testDataDir, "credentials.json");
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
||||||
@@ -206,8 +204,6 @@ describe("settings-service.ts", () => {
|
|||||||
|
|
||||||
const credentials = await settingsService.getCredentials();
|
const credentials = await settingsService.getCredentials();
|
||||||
expect(credentials.apiKeys.anthropic).toBe("sk-test");
|
expect(credentials.apiKeys.anthropic).toBe("sk-test");
|
||||||
expect(credentials.apiKeys.google).toBe("");
|
|
||||||
expect(credentials.apiKeys.openai).toBe("");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,8 +212,6 @@ describe("settings-service.ts", () => {
|
|||||||
const updates: Partial<Credentials> = {
|
const updates: Partial<Credentials> = {
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-test-key",
|
anthropic: "sk-test-key",
|
||||||
google: "",
|
|
||||||
openai: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,8 +231,6 @@ describe("settings-service.ts", () => {
|
|||||||
...DEFAULT_CREDENTIALS,
|
...DEFAULT_CREDENTIALS,
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-initial",
|
anthropic: "sk-initial",
|
||||||
google: "google-key",
|
|
||||||
openai: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const credentialsPath = path.join(testDataDir, "credentials.json");
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
||||||
@@ -253,7 +245,6 @@ describe("settings-service.ts", () => {
|
|||||||
const updated = await settingsService.updateCredentials(updates);
|
const updated = await settingsService.updateCredentials(updates);
|
||||||
|
|
||||||
expect(updated.apiKeys.anthropic).toBe("sk-updated");
|
expect(updated.apiKeys.anthropic).toBe("sk-updated");
|
||||||
expect(updated.apiKeys.google).toBe("google-key"); // Preserved
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deep merge api keys", async () => {
|
it("should deep merge api keys", async () => {
|
||||||
@@ -261,8 +252,6 @@ describe("settings-service.ts", () => {
|
|||||||
...DEFAULT_CREDENTIALS,
|
...DEFAULT_CREDENTIALS,
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-anthropic",
|
anthropic: "sk-anthropic",
|
||||||
google: "google-key",
|
|
||||||
openai: "openai-key",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const credentialsPath = path.join(testDataDir, "credentials.json");
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
||||||
@@ -270,15 +259,13 @@ describe("settings-service.ts", () => {
|
|||||||
|
|
||||||
const updates: Partial<Credentials> = {
|
const updates: Partial<Credentials> = {
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
openai: "new-openai-key",
|
anthropic: "sk-updated-anthropic",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const updated = await settingsService.updateCredentials(updates);
|
const updated = await settingsService.updateCredentials(updates);
|
||||||
|
|
||||||
expect(updated.apiKeys.anthropic).toBe("sk-anthropic");
|
expect(updated.apiKeys.anthropic).toBe("sk-updated-anthropic");
|
||||||
expect(updated.apiKeys.google).toBe("google-key");
|
|
||||||
expect(updated.apiKeys.openai).toBe("new-openai-key");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -287,34 +274,24 @@ describe("settings-service.ts", () => {
|
|||||||
const masked = await settingsService.getMaskedCredentials();
|
const masked = await settingsService.getMaskedCredentials();
|
||||||
expect(masked.anthropic.configured).toBe(false);
|
expect(masked.anthropic.configured).toBe(false);
|
||||||
expect(masked.anthropic.masked).toBe("");
|
expect(masked.anthropic.masked).toBe("");
|
||||||
expect(masked.google.configured).toBe(false);
|
|
||||||
expect(masked.openai.configured).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should mask keys correctly", async () => {
|
it("should mask keys correctly", async () => {
|
||||||
await settingsService.updateCredentials({
|
await settingsService.updateCredentials({
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-ant-api03-1234567890abcdef",
|
anthropic: "sk-ant-api03-1234567890abcdef",
|
||||||
google: "AIzaSy1234567890abcdef",
|
|
||||||
openai: "sk-1234567890abcdef",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const masked = await settingsService.getMaskedCredentials();
|
const masked = await settingsService.getMaskedCredentials();
|
||||||
expect(masked.anthropic.configured).toBe(true);
|
expect(masked.anthropic.configured).toBe(true);
|
||||||
expect(masked.anthropic.masked).toBe("sk-a...cdef");
|
expect(masked.anthropic.masked).toBe("sk-a...cdef");
|
||||||
expect(masked.google.configured).toBe(true);
|
|
||||||
expect(masked.google.masked).toBe("AIza...cdef");
|
|
||||||
expect(masked.openai.configured).toBe(true);
|
|
||||||
expect(masked.openai.masked).toBe("sk-1...cdef");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle short keys", async () => {
|
it("should handle short keys", async () => {
|
||||||
await settingsService.updateCredentials({
|
await settingsService.updateCredentials({
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "short",
|
anthropic: "short",
|
||||||
google: "",
|
|
||||||
openai: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -332,7 +309,7 @@ describe("settings-service.ts", () => {
|
|||||||
|
|
||||||
it("should return true when credentials file exists", async () => {
|
it("should return true when credentials file exists", async () => {
|
||||||
await settingsService.updateCredentials({
|
await settingsService.updateCredentials({
|
||||||
apiKeys: { anthropic: "test", google: "", openai: "" },
|
apiKeys: { anthropic: "test" },
|
||||||
});
|
});
|
||||||
const exists = await settingsService.hasCredentials();
|
const exists = await settingsService.hasCredentials();
|
||||||
expect(exists).toBe(true);
|
expect(exists).toBe(true);
|
||||||
@@ -508,8 +485,6 @@ describe("settings-service.ts", () => {
|
|||||||
state: {
|
state: {
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "sk-test-key",
|
anthropic: "sk-test-key",
|
||||||
google: "google-key",
|
|
||||||
openai: "openai-key",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -522,8 +497,6 @@ describe("settings-service.ts", () => {
|
|||||||
|
|
||||||
const credentials = await settingsService.getCredentials();
|
const credentials = await settingsService.getCredentials();
|
||||||
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
||||||
expect(credentials.apiKeys.google).toBe("google-key");
|
|
||||||
expect(credentials.apiKeys.openai).toBe("openai-key");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should migrate project settings from localStorage data", async () => {
|
it("should migrate project settings from localStorage data", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user