diff --git a/apps/cli/src/lib/telemetry.ts b/apps/cli/src/lib/telemetry.ts index 08cc8ab2..881c6070 100644 --- a/apps/cli/src/lib/telemetry.ts +++ b/apps/cli/src/lib/telemetry.ts @@ -1,3 +1,5 @@ +import path from 'node:path'; + const POSTHOG_KEY = 'phc_aUZcaccxNs56PokvsvIInqHCrwjUjvpiMWih9P86cTV'; const POSTHOG_HOST = 'https://us.i.posthog.com'; const TELEMETRY_ENV_FLAG = 'BTCA_TELEMETRY'; @@ -11,7 +13,7 @@ const expandHome = (filePath: string) => { return filePath; }; -const getTelemetryPath = () => `${expandHome(TELEMETRY_CONFIG_DIR)}/${TELEMETRY_FILENAME}`; +const getTelemetryPath = () => path.join(expandHome(TELEMETRY_CONFIG_DIR), TELEMETRY_FILENAME); type TelemetryConfig = { enabled: boolean; @@ -78,11 +80,11 @@ const ensureConfigDir = async (configDir: string) => { }; const saveTelemetryConfig = async (config: TelemetryConfig) => { - const path = getTelemetryPath(); - const configDir = path.slice(0, path.lastIndexOf('/')); + const telemetryPath = getTelemetryPath(); + const configDir = path.dirname(telemetryPath); await ensureConfigDir(configDir); - await Bun.write(`${configDir}/.keep`, ''); - await Bun.write(path, JSON.stringify(config, null, 2)); + await Bun.write(path.join(configDir, '.keep'), ''); + await Bun.write(telemetryPath, JSON.stringify(config, null, 2)); }; const getOrCreateTelemetryConfig = async () => { diff --git a/apps/server/src/resources/schema.ts b/apps/server/src/resources/schema.ts index b632e67d..2eae0db8 100644 --- a/apps/server/src/resources/schema.ts +++ b/apps/server/src/resources/schema.ts @@ -119,7 +119,7 @@ const SearchPathSchema = z .refine((path) => !path.includes('..'), { message: 'Search path must not contain path traversal sequences (..)' }) - .refine((path) => !path.startsWith('/') && !path.match(/^[a-zA-Z]:\\/), { + .refine((path) => !path.startsWith('/') && !path.match(/^[a-zA-Z]:[\\/]/), { message: 'Search path must not be an absolute path' }); @@ -161,7 +161,7 @@ const LocalPathSchema = z .refine((path) => !path.includes('\0'), { message: 'Path must not contain null bytes' }) - .refine((path) => path.startsWith('/') || path.match(/^[a-zA-Z]:\\/), { + .refine((path) => path.startsWith('/') || path.match(/^[a-zA-Z]:[\\/]/), { message: 'Local path must be an absolute path' }); diff --git a/apps/server/src/validation/index.test.ts b/apps/server/src/validation/index.test.ts index 597d7f5d..bc24e036 100644 --- a/apps/server/src/validation/index.test.ts +++ b/apps/server/src/validation/index.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'bun:test'; -import { validateResourceReference, validateResourcesArray } from './index.ts'; +import { + validateLocalPath, + validateResourceReference, + validateResourcesArray, + validateSearchPath +} from './index.ts'; describe('validateResourceReference', () => { it('accepts configured resource names', () => { @@ -72,3 +77,18 @@ describe('validateResourcesArray', () => { expect(result.valid).toBe(true); }); }); + +describe('windows path handling', () => { + it('accepts absolute windows local paths with forward slashes', () => { + const result = validateLocalPath('C:/Users/test/project'); + expect(result.valid).toBe(true); + }); + + it('rejects absolute windows search paths as searchPath', () => { + const result = validateSearchPath('C:/Users/test/project'); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain('absolute path'); + } + }); +}); diff --git a/apps/server/src/validation/index.ts b/apps/server/src/validation/index.ts index 92f920c6..23665620 100644 --- a/apps/server/src/validation/index.ts +++ b/apps/server/src/validation/index.ts @@ -416,7 +416,7 @@ export const validateSearchPath = (searchPath: string | undefined): ValidationRe } // Reject absolute paths - if (searchPath.startsWith('/') || searchPath.match(/^[a-zA-Z]:\\/)) { + if (searchPath.startsWith('/') || searchPath.match(/^[a-zA-Z]:[\\/]/)) { return fail('Search path must not be an absolute path'); } @@ -456,7 +456,7 @@ export const validateLocalPath = (path: string): ValidationResult => { } // Must be absolute path (starts with / on Unix or drive letter on Windows) - if (!normalizedPath.startsWith('/') && !normalizedPath.match(/^[a-zA-Z]:\\/)) { + if (!normalizedPath.startsWith('/') && !normalizedPath.match(/^[a-zA-Z]:[\\/]/)) { return fail('Local path must be an absolute path'); }