12-phase plan covering security fixes, proxy hardening, state bugs, content extraction, cache bounds, accessibility, and test coverage. 42 atomic tasks structured for sequential agent execution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
AIUI Debug, Fix & Hardening Plan
Execution Rules
- Run on
developmentbranch - Each
- [ ]task is one agent iteration — complete fully before moving on - After each phase:
pnpm typecheck && pnpm lint && pnpm test -- --run - If checks fail, fix before proceeding
- Commit at end of each phase with
type(scope): descriptionformat - Do NOT push — human will review and push
PHASE 0: Baseline Verification
- P0.1 — Baseline check
- Run:
pnpm install && pnpm typecheck && pnpm lint && pnpm test -- --run - Record output. Note existing failures. Fix any blockers before proceeding.
- Commit:
chore(app): verify baseline before hardening
- Run:
PHASE 1: Critical Security Fixes
-
P1.1 — Fix CORS wildcard in claude-proxy.ts
- File:
packages/app/server/claude-proxy.ts - Replace all
'Access-Control-Allow-Origin': '*'(lines ~415, 473, 531) with the correct localhost origin or importALLOWED_ORIGINfromdev-auth.ts. Match the pattern already used on lines 294/303. - Verify:
grep -n "Allow-Origin.*\*" packages/app/server/claude-proxy.tsreturns zero matches.
- File:
-
P1.2 — Fix dev auth token bypass
- File:
packages/app/server/dev-auth.ts - Line 11:
if (!token) return trueskips auth entirely when token empty. - Fix: Only skip in non-production.
if (!token) { if (process.env.NODE_ENV === 'production') { res.writeHead(401); res.end('Unauthorized'); return false; } return true; } - Add console.warn when auth disabled.
- File:
-
P1.3 — Symlink traversal protection in vite-fs.ts
- File:
packages/app/vite-fs.ts - In
walkfunction: after constructingfullPath, addif (lstatSync(fullPath).isSymbolicLink()) continue; - In
handleRead: beforestatSync, checklstatSync(filePath).isSymbolicLink()→ return 403. - Import
lstatSyncfromfs.
- File:
-
P1.4 — Body size limit on vite-fs.ts mkdir
- File:
packages/app/vite-fs.ts handleMkdirreads body with no size cap (lines 185-209).- Add
const MAX_BODY_SIZE = 1024. Tracklet size = 0ondataevents. Return 413 if exceeded.
- File:
-
P1.5 — JSON schema validation in vite-dev-chats.ts
- File:
packages/app/vite-dev-chats.ts - After
JSON.parse(body)on line 66, validate shape: must be object with optionalconversations(object) andactiveConversationId(string|null). Reject with 400 if invalid.
- File:
-
P1.6 — CSP headers in nginx-archy.conf
- File:
packages/app/server/nginx-archy.conf - Add inside
/aiui/location block:add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://api.anthropic.com https://openrouter.ai https://wavlake.com https://itunes.apple.com https://openlibrary.org https://covers.openlibrary.org https://en.wikipedia.org https://www.googleapis.com https://image.tmdb.org; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- File:
-
P1.7 — OpenRouter validation + streaming timeout
- File:
packages/app/server/claude-proxy.ts - OpenRouter handler: parse
reqBody, validate hasmodel(string),messages(array),stream(boolean). Return 400 if invalid. - Both streaming reader loops: add idle timeout (120s).
let idleTimer = setTimeout(() => reader.cancel(), 120000). Reset on each chunk. Clear on completion. - Verify:
pnpm typecheck - Commit:
fix(app): critical security — CORS, auth, symlink, CSP, body validation
- File:
PHASE 2: Proxy & Server Hardening
-
P2.1 — Fix tool use loop in claude-proxy.ts
- File:
packages/app/server/claude-proxy.ts(lines 164-222) - Unknown tool names: push error tool_result
{ type: 'tool_result', tool_use_id: tu.id, content: 'Error: unknown tool', is_error: true }. - Add turnMessages size guard:
if (JSON.stringify(turnMessages).length > 500_000)break loop. - Add
AbortSignal.timeout(30000)on each API call within loop.
- File:
-
P2.2 — SSRF mitigation in vite-rss.ts
- File:
packages/app/vite-rss.ts - After
tryParseFeedreturns, validate returned article URLs withisPrivateUrl. Filter out any private URLs. - Add comment documenting residual TOCTOU risk (dev-only middleware).
- File:
-
P2.3 — Sanitize web search context in useAI.ts
- File:
packages/app/src/composables/useAI.ts(lines 399-408) - Create helper:
function sanitizeSearchText(s: string): string { return s.replace(/[[\]()#*_~>]/g, '\$&').replace(/\n/g, ' ').slice(0, 500) }` - Apply to
r.titleandr.contentinformatWebSearchContext. - Commit:
fix(app): proxy hardening — tool loop, SSRF, prompt injection
- File:
PHASE 3: State & Logic Bugs
-
P3.1 — Fix contentType assignment in useContentPanel.ts
- File:
packages/app/src/composables/useContentPanel.ts(lines 219-225) - The
contentTyperef type is'film' | 'song' | 'podcast'. Expand to include all content types or useContentTab. Fix assignments: books→'film' is acceptable if type can't expand, but news/TV should map correctly. If the type is only used for player logic, keep narrow type but fix assignments to be semantically correct.
- File:
-
P3.2 — Fix apiFetching race in useBannerFallback.ts
- File:
packages/app/src/composables/useBannerFallback.ts - Change
let apiFetching = falsetoconst apiFetching = ref(false). Update all references to.value.
- File:
-
P3.3 — Fix event listener cleanup in chat.ts
- File:
packages/app/src/stores/chat.ts(lines 111-118) - Extract handlers to named functions. Add
import.meta.hot?.dispose()cleanup for HMR.
- File:
-
P3.4 — Fix news RSS race condition in useContentPanel.ts
- File:
packages/app/src/composables/useContentPanel.ts(lines 134-154) - Capture
const tabAtStart = activeTab.valuebefore RSS fetch. In.then(), only setactiveTab.value = 'news'ifactiveTab.value === tabAtStart(user hasn't manually switched).
- File:
-
P3.5 — Add error logging to silent catch blocks
- Files:
useImageFallback.ts,useAI.ts,stores/chat.ts - Replace all
catch { }andcatch { /* ignore */ }withcatch(e) { console.debug('[module] op failed:', e) }. - Keep
console.debug(noterror) for expected failures. - Commit:
fix(app): state bugs — contentType, race conditions, error logging
- Files:
PHASE 4: Content Extraction & Filtering
-
P4.1 — Add recipe instructions to system prompt
- File:
packages/app/src/composables/useAI.ts(SYSTEM_PROMPT) - Add after Apps section:
**Recipes:** When sharing recipes, use the <recipe_ext> XML tag: <recipe_ext title="Name" servings="4" time="30 min" calories="450"> - ingredient 1 1. Step one </recipe_ext>
- File:
-
P4.2 — Fix app category validation
- File:
packages/app/src/composables/contentExtraction.ts(line ~1330) - Add
const VALID_CATEGORIES = new Set(['nostr-client','lightning-wallet','bitcoin-wallet','privacy','node','dev-tool','relay']). Validate before cast, fallback to'dev-tool'.
- File:
-
P4.3 — Fix song deduplication
- File:
packages/app/src/composables/contentExtraction.ts(lines 606-638) - After combining library + external songs, deduplicate by normalized
title|artistkey. Library songs (with real IDs) take priority.
- File:
-
P4.4 — Fix film/TV tag ambiguity
- File:
packages/app/src/composables/contentExtraction.ts(lines 914-916) - Only convert
film_extto TV when: query is TV-like AND no explicittv_exttags present AND response reads TV-like. If AI used bothfilm_extandtv_ext, keep both as-is.
- File:
-
P4.5 — Fix isBookLikeResponse false positives
- File:
packages/app/src/composables/contentFiltering.ts(lines 60-62) - Increase
by [Author]threshold from>= 1to>= 2.
- File:
-
P4.6 — Fix isRecipeLikeResponse for prose recipes
- File:
packages/app/src/composables/contentFiltering.ts(lines 92-94) - Add prose detection: recipe keywords + list structure pattern.
- File:
-
P4.7 — Fix looksLikeSong false positives
- File:
packages/app/src/composables/contentExtraction.ts(lines 488-506) - Add financial terms to blocklist:
'market cap','etf','price','trading','volume','earnings','valuation','stock','portfolio','investment','yield','inflation','interest rate'. - Commit:
fix(app): extraction — dedup, tag ambiguity, false positives, recipes
- File:
PHASE 5: Memory & Cache Hardening
-
P5.1 — Bound caches in useImageFallback.ts
- File:
packages/app/src/composables/useImageFallback.ts - Create helper:
function boundedSet<K,V>(map: Map<K,V>, key: K, val: V, max=500) { if (map.size >= max) { const first = map.keys().next().value; if (first !== undefined) map.delete(first); } map.set(key, val); } - Replace all
.set()calls on memory caches withboundedSet(). - Also bound
failedUrlsSet to 1000 entries.
- File:
-
P5.2 — Bound caches in usePlayer.ts + cleanup
- File:
packages/app/src/composables/usePlayer.ts - Bound
resultCacheandnullCacheTimestampsto 200 entries. - Add
destroy()method that callsdestroyPlayer(), resets DOM refs, cancels active search controller. - Commit:
fix(app): bound caches, player cleanup
- File:
PHASE 6: Accessibility
- P6.1 — Fix touch targets and aria attributes
- Audit all
*Grid.vueand*Detail.vuefor: interactive elements < 44px, images without alt, SVG fallbacks without aria-label. - ImageGrid.vue: add
role="img"aria-label="Image unavailable"to fallback SVG wrapper. - Ensure all interactive genre/tag pills in grids have adequate touch targets (min 44x44 including padding).
- Non-interactive info pills are exempt.
- Commit:
fix(app): accessibility — touch targets, alt text, aria
- Audit all
PHASE 7: Security Tests
-
P7.1 — Write dev-auth tests
- New file:
packages/app/src/__tests__/dev-auth.test.ts - Tests: auth bypass when no token (dev), auth required when token set, rate limiting 429, CORS headers correct.
- New file:
-
P7.2 — Extend proxy tests
- File:
packages/app/src/__tests__/proxy.test.ts - Tests: unknown tool error result, max rounds termination, turnMessages size guard, streaming timeout.
- File:
-
P7.3 — Write vite-fs security tests
- New file:
packages/app/src/__tests__/vite-fs.test.ts - Tests: path traversal rejected, symlink blocked, sensitive files blocked, body size limit enforced.
- New file:
-
P7.4 — Write SSRF tests for vite-rss
- New file:
packages/app/src/__tests__/vite-rss.test.ts - Tests: private IP detection, localhost blocked, non-http blocked, private URLs filtered from results.
- Commit:
test(app): security tests — auth, proxy, fs, SSRF
- New file:
PHASE 8: Content Extraction Tests
-
P8.1 — Write content filtering tests
- New file:
packages/app/src/composables/__tests__/contentFiltering.test.ts - Tests: isBookLikeResponse false positive fix, isRecipeLikeResponse prose detection, isMusicQuery rejects financial terms, filterTabsByContext ordering.
- New file:
-
P8.2 — Extend content extraction tests
- File:
packages/app/src/__tests__/contentExtraction.test.ts - Tests: song dedup, looksLikeSong rejects financial terms, app category fallback, recipe parsing.
- File:
-
P8.3 — Write TV/film resolution tests
- Same file as P8.2.
- Tests: film_ext→TV conversion rules, book_ext in news context, explicit tags respected.
- Commit:
test(app): content extraction — filtering, dedup, tag resolution
PHASE 9: State & Composable Tests
-
P9.1 — Write useContentPanel tests
- File:
packages/app/src/composables/__tests__/useContentPanel.test.ts - Tests: contentType assignment per content type, RSS tab override prevention, closePanel resets, empty text → no tabs.
- File:
-
P9.2 — Write useBannerFallback tests
- New file:
packages/app/src/composables/__tests__/useBannerFallback.test.ts - Tests: primary URL fallthrough, API fetch on exhaustion, apiFetching guard, gradient fallback.
- New file:
-
P9.3 — Write chat store tests
- New file:
packages/app/src/__tests__/chat-store.test.ts - Tests: createConversation, addMessage, deleteConversation, branchFromMessage, debouncedIDBSave.
- Commit:
test(app): state tests — contentPanel, bannerFallback, chat store
- New file:
PHASE 10: AI Integration Tests
-
P10.1 — Extend useAI tests
- File:
packages/app/src/__tests__/useAI.test.ts - Tests: system prompt includes recipe instructions, web search results sanitized, editAndResend truncates, sanitizeHistory merges consecutive roles.
- File:
-
P10.2 — Write useImageFallback cache tests
- New file:
packages/app/src/composables/__tests__/useImageFallback.test.ts - Tests: boundedSet evicts at max, generatePosterFallback valid SVG, escapeXml handles specials.
- Commit:
test(app): AI integration, image fallback cache tests
- New file:
PHASE 11: Final Verification
-
P11.1 — Full integration check
- Run:
pnpm typecheck && pnpm lint && pnpm test -- --run - Run:
pnpm build— verify production build succeeds - Fix any regressions.
- Run:
-
P11.2 — Final commit
- Run coverage report if configured:
pnpm test -- --run --coverage - Ensure all changes committed on
development. - Commit:
chore(app): hardening complete — all checks pass
- Run coverage report if configured:
Phase Dependencies
P0 → P1 → P2 → P3 → P4 → P5 ─┐
├→ P7 (tests P1,P2)
P6 ──┤
├→ P8 (tests P4)
├→ P9 (tests P3,P5)
├→ P10 (tests P2.3,P4.1)
└→ P11 (final)
P5 and P6 can run in parallel. P7-P10 can run in any order after their fix phases.
Summary
- 12 phases, 42 tasks
- Phases 0-6: fixes (security → proxy → state → extraction → caches → a11y)
- Phases 7-10: tests (security → extraction → state → AI)
- Phase 11: final verification
- Target: all checks pass, 40%+ test coverage on critical paths, secure for Archy deployment