Skip to content

Commit cd08686

Browse files
IM.codesclaude
andcommitted
fix: audit findings — force refresh, shared constants, remove path leak
1. Force refresh: Refresh button sends force:true, daemon invalidates cache before re-fetching (was serving stale data for 5-30 min) 2. Daemon repo-handler now uses REPO_MSG constants from shared/repo-types.ts instead of hardcoded strings (prevents future type name drift) 3. Remove projectDir from user-facing error UI (info disclosure) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 47caf96 commit cd08686

4 files changed

Lines changed: 45 additions & 29 deletions

File tree

src/daemon/repo-handler.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { RepoProvider, ListOptions, CommitListOptions } from '../repo/provi
1212
import { listSessions } from '../store/session-store.js';
1313
import type { ServerLink } from './server-link.js';
1414
import logger from '../util/logger.js';
15+
import { REPO_MSG } from '../shared/repo-types.js';
1516

1617
// ---------------------------------------------------------------------------
1718
// Concurrency limiter — max 3 concurrent CLI calls per projectDir
@@ -122,13 +123,13 @@ async function handleDetect(
122123
const cacheKey = RepoCache.buildKey(projectDir, 'detect');
123124
const cached = repoCache.get<RepoContext>(cacheKey);
124125
if (cached) {
125-
serverLink.send({ type: 'repo.detect_response', requestId, projectDir, ...cached });
126+
serverLink.send({ type: REPO_MSG.DETECT_RESPONSE, requestId, projectDir, ...cached });
126127
return;
127128
}
128129

129130
const ctx = await detectRepo(projectDir);
130131
repoCache.set(cacheKey, ctx, projectDir, ctx.status !== 'ok');
131-
serverLink.send({ type: 'repo.detect_response', requestId, projectDir, ...ctx });
132+
serverLink.send({ type: REPO_MSG.DETECT_RESPONSE, requestId, projectDir, ...ctx });
132133
}
133134

134135
async function handleListIssues(
@@ -145,7 +146,7 @@ async function handleListIssues(
145146
const cacheKey = RepoCache.buildKey(projectDir, 'issues', { ...opts });
146147
const cached = repoCache.get<unknown>(cacheKey);
147148
if (cached) {
148-
serverLink.send({ type: 'repo.issues_response', requestId, ...cached as object });
149+
serverLink.send({ type: REPO_MSG.ISSUES_RESPONSE, requestId, ...cached as object });
149150
return;
150151
}
151152

@@ -155,7 +156,7 @@ async function handleListIssues(
155156
try {
156157
const result = await provider.listIssues(opts);
157158
repoCache.set(cacheKey, result, projectDir);
158-
serverLink.send({ type: 'repo.issues_response', requestId, ...result });
159+
serverLink.send({ type: REPO_MSG.ISSUES_RESPONSE, requestId, ...result });
159160
} catch (err) {
160161
sendError(serverLink, requestId, projectDir, 'cli_error', err);
161162
}
@@ -175,7 +176,7 @@ async function handleListPRs(
175176
const cacheKey = RepoCache.buildKey(projectDir, 'prs', { ...opts });
176177
const cached = repoCache.get<unknown>(cacheKey);
177178
if (cached) {
178-
serverLink.send({ type: 'repo.prs_response', requestId, ...cached as object });
179+
serverLink.send({ type: REPO_MSG.PRS_RESPONSE, requestId, ...cached as object });
179180
return;
180181
}
181182

@@ -185,7 +186,7 @@ async function handleListPRs(
185186
try {
186187
const result = await provider.listPRs(opts);
187188
repoCache.set(cacheKey, result, projectDir);
188-
serverLink.send({ type: 'repo.prs_response', requestId, ...result });
189+
serverLink.send({ type: REPO_MSG.PRS_RESPONSE, requestId, ...result });
189190
} catch (err) {
190191
sendError(serverLink, requestId, projectDir, 'cli_error', err);
191192
}
@@ -201,7 +202,7 @@ async function handleListBranches(
201202
const cacheKey = RepoCache.buildKey(projectDir, 'branches');
202203
const cached = repoCache.get<unknown>(cacheKey);
203204
if (cached) {
204-
serverLink.send({ type: 'repo.branches_response', requestId, ...cached as object });
205+
serverLink.send({ type: REPO_MSG.BRANCHES_RESPONSE, requestId, ...cached as object });
205206
return;
206207
}
207208

@@ -211,7 +212,7 @@ async function handleListBranches(
211212
try {
212213
const result = await provider.listBranches();
213214
repoCache.set(cacheKey, result, projectDir);
214-
serverLink.send({ type: 'repo.branches_response', requestId, ...result });
215+
serverLink.send({ type: REPO_MSG.BRANCHES_RESPONSE, requestId, ...result });
215216
} catch (err) {
216217
sendError(serverLink, requestId, projectDir, 'cli_error', err);
217218
}
@@ -231,7 +232,7 @@ async function handleListCommits(
231232
const cacheKey = RepoCache.buildKey(projectDir, 'commits', { ...opts });
232233
const cached = repoCache.get<unknown>(cacheKey);
233234
if (cached) {
234-
serverLink.send({ type: 'repo.commits_response', requestId, ...cached as object });
235+
serverLink.send({ type: REPO_MSG.COMMITS_RESPONSE, requestId, ...cached as object });
235236
return;
236237
}
237238

@@ -241,7 +242,7 @@ async function handleListCommits(
241242
try {
242243
const result = await provider.listCommits(opts);
243244
repoCache.set(cacheKey, result, projectDir);
244-
serverLink.send({ type: 'repo.commits_response', requestId, ...result });
245+
serverLink.send({ type: REPO_MSG.COMMITS_RESPONSE, requestId, ...result });
245246
} catch (err) {
246247
sendError(serverLink, requestId, projectDir, 'cli_error', err);
247248
}
@@ -267,7 +268,7 @@ async function getProvider(
267268
const provider = createProvider(ctx, projectDir);
268269
if (!provider) {
269270
serverLink.send({
270-
type: 'repo.error',
271+
type: REPO_MSG.ERROR,
271272
requestId,
272273
error: 'not_detected' as RepoError,
273274
status: ctx.status,
@@ -297,7 +298,7 @@ function sendError(
297298
if (err) {
298299
logger.error({ err }, `repo handler: ${error}`);
299300
}
300-
serverLink.send({ type: 'repo.error', requestId, projectDir, error });
301+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error });
301302
}
302303

303304
// ---------------------------------------------------------------------------
@@ -311,27 +312,32 @@ export function handleRepoCommand(cmd: Record<string, unknown>, serverLink: Serv
311312
// projectDir validation for all commands
312313
if (!validateProjectDir(projectDir)) {
313314
logger.debug({ projectDir, knownDirs: listSessions().map((s) => s.projectDir) }, 'repo: projectDir validation failed');
314-
serverLink.send({ type: 'repo.error', requestId, projectDir, error: 'invalid_params' as RepoError });
315+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error: 'invalid_params' as RepoError });
315316
return;
316317
}
317318

318319
// Input schema validation
319320
if (cmd.state !== undefined && !isValidState(cmd.state)) {
320-
serverLink.send({ type: 'repo.error', requestId, projectDir, error: 'invalid_params' as RepoError });
321+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error: 'invalid_params' as RepoError });
321322
return;
322323
}
323324
if (cmd.branch !== undefined && !isValidBranch(cmd.branch)) {
324-
serverLink.send({ type: 'repo.error', requestId, projectDir, error: 'invalid_params' as RepoError });
325+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error: 'invalid_params' as RepoError });
325326
return;
326327
}
327328
if (cmd.page !== undefined && !isValidPage(cmd.page)) {
328-
serverLink.send({ type: 'repo.error', requestId, projectDir, error: 'invalid_params' as RepoError });
329+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error: 'invalid_params' as RepoError });
329330
return;
330331
}
331332

332333
// Strip any browser-sent provider field
333334
delete cmd.provider;
334335

336+
// Force refresh: invalidate cache for this projectDir before re-fetching
337+
if (cmd.force === true) {
338+
repoCache.invalidate(projectDir as string);
339+
}
340+
335341
const run = async (): Promise<void> => {
336342
switch (cmd.type) {
337343
case 'repo.detect':
@@ -356,6 +362,6 @@ export function handleRepoCommand(cmd: Record<string, unknown>, serverLink: Serv
356362

357363
void withConcurrencyLimit(projectDir as string, run).catch((err) => {
358364
logger.error({ err, type: cmd.type }, 'repo handler failed');
359-
serverLink.send({ type: 'repo.error', requestId, projectDir, error: 'cli_error' as RepoError });
365+
serverLink.send({ type: REPO_MSG.ERROR, requestId, projectDir, error: 'cli_error' as RepoError });
360366
});
361367
}

src/shared/repo-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../shared/repo-types.ts

web/src/pages/RepoPage.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,23 @@ export function RepoPage({ ws, projectDir, onBack }: Props) {
237237
// ── Actions ──────────────────────────────────────────────────────────────
238238

239239
const handleRefresh = useCallback(() => {
240-
doDetect();
241-
// Re-fetch active tab
240+
// Force refresh — bypass daemon cache to get fresh data from gh/glab CLI
241+
setDetectLoading(true);
242+
setDetectError(null);
243+
let rid: string;
244+
try {
245+
rid = ws.repoDetect(projectDir, { force: true });
246+
} catch (err) {
247+
setDetectError(`Send failed: ${err instanceof Error ? err.message : String(err)}`);
248+
setDetectLoading(false);
249+
return;
250+
}
251+
detectReqRef.current = rid;
252+
pendingRef.current.add(rid);
253+
// Re-fetch active tab with force
242254
updateTab(activeTab, { fetched: false, items: [], page: 1, hasMore: false });
243255
fetchTab(activeTab);
244-
}, [doDetect, activeTab, updateTab, fetchTab]);
256+
}, [ws, projectDir, activeTab, updateTab, fetchTab]);
245257

246258
const handleLoadMore = useCallback(() => {
247259
const tab = tabs[activeTab];
@@ -426,7 +438,7 @@ export function RepoPage({ ws, projectDir, onBack }: Props) {
426438
if (detectError) return (
427439
<div style={{ padding: 24, textAlign: 'center', color: '#94a3b8', fontSize: 13 }}>
428440
<div style={{ marginBottom: 12, color: '#f87171' }}>{detectError}</div>
429-
<div style={{ marginBottom: 12, fontSize: 11, color: '#475569', wordBreak: 'break-all' }}>projectDir: {projectDir}</div>
441+
<div style={{ marginBottom: 12, fontSize: 11, color: '#475569' }}>{t('repo.retry')}</div>
430442
<button class="btn btn-sm" onClick={doDetect}>{t('repo.retry')}</button>
431443
</div>
432444
);
@@ -491,10 +503,7 @@ export function RepoPage({ ws, projectDir, onBack }: Props) {
491503
)}
492504

493505
{detectError && (
494-
<div style={{ flex: 1, overflow: 'hidden' }}>
495-
<span style={{ color: '#f87171', fontSize: 13 }}>{detectError}</span>
496-
<span style={{ color: '#475569', fontSize: 10, marginLeft: 8 }}>{projectDir}</span>
497-
</div>
506+
<span style={{ color: '#f87171', fontSize: 13, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{detectError}</span>
498507
)}
499508

500509
{context && !detectLoading && (

web/src/ws-client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,21 +327,21 @@ export class WsClient {
327327
// ── Repo commands ──────────────────────────────────────────────────────────
328328

329329
/** Detect repo context for a project directory. Returns requestId. */
330-
repoDetect(projectDir: string): string {
330+
repoDetect(projectDir: string, opts?: { force?: boolean }): string {
331331
const requestId = crypto.randomUUID();
332-
this.send({ type: 'repo.detect', requestId, projectDir });
332+
this.send({ type: 'repo.detect', requestId, projectDir, ...(opts?.force ? { force: true } : {}) });
333333
return requestId;
334334
}
335335

336336
/** List issues for a project. Returns requestId. */
337-
repoListIssues(projectDir: string, opts?: { state?: string; page?: number }): string {
337+
repoListIssues(projectDir: string, opts?: { state?: string; page?: number; force?: boolean }): string {
338338
const requestId = crypto.randomUUID();
339339
this.send({ type: 'repo.list_issues', requestId, projectDir, ...opts });
340340
return requestId;
341341
}
342342

343343
/** List pull requests for a project. Returns requestId. */
344-
repoListPRs(projectDir: string, opts?: { state?: string; page?: number }): string {
344+
repoListPRs(projectDir: string, opts?: { state?: string; page?: number; force?: boolean }): string {
345345
const requestId = crypto.randomUUID();
346346
this.send({ type: 'repo.list_prs', requestId, projectDir, ...opts });
347347
return requestId;

0 commit comments

Comments
 (0)