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;
}