diff --git a/apps/server/src/routes/github/routes/list-comments.ts b/apps/server/src/routes/github/routes/list-comments.ts index 1813923e..2c057709 100644 --- a/apps/server/src/routes/github/routes/list-comments.ts +++ b/apps/server/src/routes/github/routes/list-comments.ts @@ -43,6 +43,16 @@ interface GraphQLResponse { errors?: Array<{ message: string }>; } +/** Timeout for GitHub API requests in milliseconds */ +const GITHUB_API_TIMEOUT_MS = 30000; + +/** + * Validate cursor format (GraphQL cursors are typically base64 strings) + */ +function isValidCursor(cursor: string): boolean { + return /^[A-Za-z0-9+/=]+$/.test(cursor); +} + /** * Fetch comments for a specific issue using GitHub GraphQL API */ @@ -53,33 +63,45 @@ async function fetchIssueComments( issueNumber: number, cursor?: string ): Promise { - const cursorParam = cursor ? `, after: "${cursor}"` : ''; + // Validate cursor format to prevent potential injection + if (cursor && !isValidCursor(cursor)) { + throw new Error('Invalid cursor format'); + } - const query = `{ - repository(owner: "${owner}", name: "${repo}") { - issue(number: ${issueNumber}) { - comments(first: 50${cursorParam}) { - totalCount - pageInfo { - hasNextPage - endCursor - } - nodes { - id - author { - login - avatarUrl + // Use GraphQL variables instead of string interpolation for safety + const query = ` + query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + comments(first: 50, after: $cursor) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + id + author { + login + avatarUrl + } + body + createdAt + updatedAt } - body - createdAt - updatedAt } } } - } - }`; + }`; - const requestBody = JSON.stringify({ query }); + const variables = { + owner, + repo, + issueNumber, + cursor: cursor || null, + }; + + const requestBody = JSON.stringify({ query, variables }); const response = await new Promise((resolve, reject) => { const gh = spawn('gh', ['api', 'graphql', '--input', '-'], { @@ -87,12 +109,19 @@ async function fetchIssueComments( env: execEnv, }); + // Add timeout to prevent hanging indefinitely + const timeoutId = setTimeout(() => { + gh.kill(); + reject(new Error('GitHub API request timed out')); + }, GITHUB_API_TIMEOUT_MS); + let stdout = ''; let stderr = ''; gh.stdout.on('data', (data: Buffer) => (stdout += data.toString())); gh.stderr.on('data', (data: Buffer) => (stderr += data.toString())); gh.on('close', (code) => { + clearTimeout(timeoutId); if (code !== 0) { return reject(new Error(`gh process exited with code ${code}: ${stderr}`)); } diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index d7c9ffc0..8d51c4b1 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -21,6 +21,8 @@ import { issueValidationSchema, ISSUE_VALIDATION_SYSTEM_PROMPT, buildValidationPrompt, + ValidationComment, + ValidationLinkedPR, } from './validation-schema.js'; import { trySetValidationRunning, @@ -35,24 +37,6 @@ import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** Valid model values for validation */ const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; -/** - * Comment structure for validation prompt - */ -interface ValidationComment { - author: string; - createdAt: string; - body: string; -} - -/** - * Linked PR structure for validation prompt - */ -interface ValidationLinkedPR { - number: number; - title: string; - state: string; -} - /** * Request body for issue validation */ diff --git a/apps/server/src/routes/github/routes/validation-schema.ts b/apps/server/src/routes/github/routes/validation-schema.ts index e33a2bda..010fcd7f 100644 --- a/apps/server/src/routes/github/routes/validation-schema.ts +++ b/apps/server/src/routes/github/routes/validation-schema.ts @@ -155,7 +155,7 @@ Be thorough in your analysis but focus on files that are directly relevant to th /** * Comment data structure for validation prompt */ -interface ValidationComment { +export interface ValidationComment { author: string; createdAt: string; body: string; @@ -164,7 +164,7 @@ interface ValidationComment { /** * Linked PR data structure for validation prompt */ -interface ValidationLinkedPR { +export interface ValidationLinkedPR { number: number; title: string; state: string; @@ -207,7 +207,9 @@ export function buildValidationPrompt( // Limit to most recent 10 comments to control prompt size const recentComments = comments.slice(-10); const commentsText = recentComments - .map((c) => `**${c.author}** (${new Date(c.createdAt).toLocaleDateString()}):\n${c.body}`) + .map( + (c) => `**${c.author}** (${new Date(c.createdAt).toISOString().slice(0, 10)}):\n${c.body}` + ) .join('\n\n---\n\n'); commentsSection = `\n\n### Comments (${comments.length} total${comments.length > 10 ? ', showing last 10' : ''})\n\n${commentsText}`; diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts index e0505b36..b5b3534f 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts @@ -101,6 +101,7 @@ export function useIssueComments(issueNumber: number | null): UseIssueCommentsRe setHasNextPage(false); setEndCursor(undefined); setLoading(false); + setError(null); } return () => {