diff --git a/.env.example b/.env.example index 7148949..1af7bd7 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ AI_MODEL='ai_model_name_here' OIDC_ENDPOINT=https://oidc-provider.org OIDC_CLIENT_ID=your_oidc_client_id OIDC_CLIENT_SECRET=your_oidc_client_secret +AUTH_SECRET=your_super_secret_jwt_key_that_should_be_at_least_32_characters_long_for_security AUTHENTICATION_NECESSARY=false # Always set this to true in prod diff --git a/package.json b/package.json index a886c00..1e27fa3 100644 --- a/package.json +++ b/package.json @@ -8,23 +8,22 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "deploy:static": "./scripts/deploy-static.sh", - "deploy:node": "./scripts/deploy-node.sh", "check:health": "curl -f http://localhost:3000/health || exit 1" }, "dependencies": { - "astro": "^5.3.0", "@astrojs/node": "^9.3.0", - "js-yaml": "^4.1.0", - "jose": "^5.2.0", + "astro": "^5.3.0", "cookie": "^0.6.0", - "dotenv": "^16.4.5" + "dotenv": "^16.4.5", + "jose": "^5.2.0", + "js-yaml": "^4.1.0", + "zod": "^3.25.76" }, "devDependencies": { - "@types/js-yaml": "^4.0.9", - "@types/cookie": "^0.6.0" + "@types/cookie": "^0.6.0", + "@types/js-yaml": "^4.0.9" }, "engines": { "node": ">=18.0.0" } -} \ No newline at end of file +} diff --git a/src/components/AIQueryInterface.astro b/src/components/AIQueryInterface.astro index 0dc2df5..a56fbbc 100644 --- a/src/components/AIQueryInterface.astro +++ b/src/components/AIQueryInterface.astro @@ -88,6 +88,12 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th \ No newline at end of file diff --git a/src/pages/api/ai/query.ts b/src/pages/api/ai/query.ts index f4b3a89..9cf1833 100644 --- a/src/pages/api/ai/query.ts +++ b/src/pages/api/ai/query.ts @@ -16,31 +16,24 @@ function getEnv(key: string): string { } const AI_MODEL = getEnv('AI_MODEL'); -// Rate limiting store (in production, use Redis) const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute const RATE_LIMIT_MAX = 10; // 10 requests per minute per user // Input validation and sanitization function sanitizeInput(input: string): string { - // Remove potential prompt injection patterns - const dangerous = [ - /ignore\s+previous\s+instructions?/gi, - /new\s+instructions?:/gi, - /system\s*:/gi, - /assistant\s*:/gi, - /human\s*:/gi, - /<\s*\/?system\s*>/gi, - /```\s*system/gi, - ]; + // Remove any content that looks like system instructions + let sanitized = input + .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks + .replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags + .replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]') + .replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]') + .trim(); - let sanitized = input.trim(); - dangerous.forEach(pattern => { - sanitized = sanitized.replace(pattern, '[FILTERED]'); - }); + // Limit length and remove excessive whitespace + sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' '); - // Limit length - return sanitized.slice(0, 2000); + return sanitized; } // Strip markdown code blocks from AI response @@ -70,6 +63,17 @@ function checkRateLimit(userId: string): boolean { return true; } +function cleanupExpiredRateLimits() { + const now = Date.now(); + for (const [userId, limit] of rateLimitStore.entries()) { + if (now > limit.resetTime) { + rateLimitStore.delete(userId); + } + } +} + +setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000); + // Load tools database async function loadToolsDatabase() { try { diff --git a/src/pages/api/auth/callback.ts b/src/pages/api/auth/callback.ts index 604b364..cae9401 100644 --- a/src/pages/api/auth/callback.ts +++ b/src/pages/api/auth/callback.ts @@ -10,11 +10,13 @@ import { export const GET: APIRoute = async ({ url, request }) => { try { - // Debug: multiple ways to access URL parameters - console.log('Full URL:', url.toString()); - console.log('URL pathname:', url.pathname); - console.log('URL search:', url.search); - console.log('URL searchParams:', url.searchParams.toString()); + if (process.env.NODE_ENV === 'development') { + console.log('Auth callback processing...'); + console.log('Full URL:', url.toString()); + console.log('URL pathname:', url.pathname); + console.log('URL search:', url.search); + console.log('URL searchParams:', url.searchParams.toString()); + } // Try different ways to get parameters const allParams = Object.fromEntries(url.searchParams.entries()); diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts new file mode 100644 index 0000000..d4fdfac --- /dev/null +++ b/src/pages/api/health.ts @@ -0,0 +1,14 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +export const GET: APIRoute = async () => { + return new Response(JSON.stringify({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); +}; \ No newline at end of file diff --git a/src/utils/auth.ts b/src/utils/auth.ts index c9c28bd..52a916a 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -14,7 +14,7 @@ function getEnv(key: string): string { return value; } -const SECRET_KEY = new TextEncoder().encode(getEnv('OIDC_CLIENT_SECRET')); +const SECRET_KEY = new TextEncoder().encode(getEnv('AUTH_SECRET')); const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds export interface SessionData { @@ -74,12 +74,13 @@ export function getSessionFromRequest(request: Request): string | null { // Create session cookie export function createSessionCookie(token: string): string { const publicBaseUrl = getEnv('PUBLIC_BASE_URL'); - const isSecure = publicBaseUrl.startsWith('https://'); + const isProduction = process.env.NODE_ENV === 'production'; + const isSecure = publicBaseUrl.startsWith('https://') || isProduction; return serialize('session', token, { httpOnly: true, secure: isSecure, - sameSite: 'lax', + sameSite: 'strict', // More secure than 'lax' maxAge: SESSION_DURATION, path: '/' }); @@ -127,7 +128,7 @@ export async function exchangeCodeForTokens(code: string): Promise { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}` + 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` }, body: new URLSearchParams({ grant_type: 'authorization_code', diff --git a/src/utils/dataService.ts b/src/utils/dataService.ts index c3132de..2b4bce0 100644 --- a/src/utils/dataService.ts +++ b/src/utils/dataService.ts @@ -1,6 +1,43 @@ import { promises as fs } from 'fs'; import { load } from 'js-yaml'; import path from 'path'; +import { z } from 'zod'; + +const ToolSchema = z.object({ + name: z.string(), + description: z.string(), + domains: z.array(z.string()).optional().nullable().default([]), + phases: z.array(z.string()).optional().nullable().default([]), + platforms: z.array(z.string()).default([]), + skillLevel: z.string(), + url: z.string(), + license: z.string(), + tags: z.array(z.string()).default([]), + // Optional fields that can be null, undefined, or empty + projectUrl: z.string().optional().nullable(), + knowledgebase: z.boolean().optional().nullable(), + statusUrl: z.string().optional().nullable(), + accessType: z.string().optional(), + 'domain-agnostic-software': z.array(z.string()).optional().nullable(), +}); + +const ToolsDataSchema = z.object({ + tools: z.array(ToolSchema), + domains: z.array(z.object({ + id: z.string(), + name: z.string() + })), + phases: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional() + })), + 'domain-agnostic-software': z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional() + })).optional().default([]), +}); interface ToolsData { tools: any[]; @@ -49,7 +86,14 @@ async function loadRawData(): Promise { if (!cachedData) { const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml'); const yamlContent = await fs.readFile(yamlPath, 'utf8'); - cachedData = load(yamlContent) as ToolsData; + const rawData = load(yamlContent); + + try { + cachedData = ToolsDataSchema.parse(rawData); + } catch (error) { + console.error('YAML validation failed:', error); + throw new Error('Invalid tools.yaml structure'); + } } return cachedData; }