SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
- [lucide-react icon name collisions ](lucide-icon-name-collisions.md ) — `Badge` /`Activity` from lucide collide with shadcn/ui Badge and React 19 Activity; import Badge from ui, Activity from lucide.
2026-06-10 13:56:15 +00:00
- [OpenAI gpt-5 temperature ](openai-temperature-gpt5.md ) — gpt-5* reject `temperature != 1` ; omit temperature in OpenAI-compatible clients or AI analysis silently fails.
2026-06-10 18:53:17 +00:00
- [NDJSON streaming on Replit ](ndjson-streaming-express-replit.md ) — use `res.on("close")` +`writableFinished` (NOT `req.on("close")` ); persist on disconnect; proxy doesn't buffer; gate fallback to avoid dup rows.
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
- [Skill fingerprint & relation matching ](skill-fingerprint-matching.md ) — don't put display name in fingerprint path; match modified by file-path Jaccard (hash-Jaccard misses single-file edits), report content-aware similarity.
Add skill version timeline (fingerprint lineage)
Task #14: show a full version timeline for each skill family, not just the
single most-similar prior scan.
What changed:
- OpenAPI spec (lib/api-spec/openapi.yaml): new GET /scans/{id}/lineage
(operationId getScanLineage) returning an array of ScanLineageEntry
(id, name, verdict, riskScore, relation, similarity, comparedScanId,
fingerprint, createdAt). Regenerated api-zod + api-client-react via codegen.
- API (artifacts/api-server/src/routes/scans.ts): new lineage endpoint.
Builds an undirected graph over all scans linked by the comparedScanId chain
AND identical (non-empty) fingerprints, then BFS-walks the connected
component containing the requested scan and returns it newest-first. Works
purely from existing data, no re-scanning. 404 for unknown ids.
- UI (artifacts/skillguard/src/pages/scan-report.tsx): new VersionTimeline
card rendering the family as a vertical timeline; each entry shows verdict,
relation badge, similarity, risk score and date. The viewed scan is marked
"Aktuell angezeigt"; every other entry links to the existing comparison view
/vergleich/{viewedId}/{entryId}. Card hidden when the family has <=1 member.
Notes:
- Lineage = connected component, so any member returns the full family.
- Verified end-to-end locally (created new/modified/identical chain, checked
lineage ordering + 404, confirmed timeline + compare links in the UI),
then deleted the test scans.
Replit-Task-Id: c7f87ce6-59d8-4396-b16b-f20846f42f0b
2026-06-10 19:47:39 +00:00
- [Testing api-server from shell ](api-server-local-curl.md ) — external `$REPLIT_DEV_DOMAIN/api` curl returns HTTP 000; curl `http://localhost:<PORT>/api` instead (port from workflow log).
2026-06-10 21:19:54 +00:00
- [Stale codegen & unapplied migrations ](skillguard-stale-codegen-and-migrations.md ) — "field already in API" tasks: dev/test DB + lib `dist/*.d.ts` lag; run drizzle push + `tsc -b` the lib.
Add automated tests for model discovery (POST /providers/list-models)
Task #25: the model-discovery capability (list available models, used by the
guided provider setup) had no automated coverage. Added a new vitest suite that
exercises the endpoint end-to-end against the in-process Express app.
New file:
- artifacts/api-server/src/routes/providers.listModels.test.ts
Coverage (6 tests, all passing):
- ok=false + clear German message when no token (empty token, no providerId),
and the upstream provider is never called.
- Falls back to the stored provider token when providerId is given and apiToken
is empty (inserts a real provider row, asserts the Bearer header carries the
stored token, cleans up afterward).
- Normalizes the OpenAI-compatible response (data[].id) into a deduped, sorted
model list; drops non-string ids.
- Anthropic path: GET /models with x-api-key + anthropic-version headers (no
Authorization), reads models[] with id/name fallback, dedupes.
- Upstream failure returns ok=false (HTTP 200, not 500), empty models, and the
token is redacted from the message ([REDACTED], never the raw token).
- fetch throwing (network error) returns ok=false without leaking the token.
Implementation note: the suite runs the app in-process and the test client also
uses fetch, so global fetch is mocked with a passthrough — requests to the test
server's baseUrl delegate to the captured real fetch; only upstream provider URLs
are synthesized. Spy assertions filter out the localhost passthrough call.
Saved this non-obvious testing lesson to memory.
Deviation / note: pre-existing failures in relation.test.ts and compare.test.ts
are unrelated to this task — the dev database's scans table is missing the
fingerprint/relation/similarity/compared_scan_id columns (schema drift; needs a
drizzle-kit push). Out of scope for this task; proposed as a follow-up.
Replit-Task-Id: 7e8a3db2-0da7-40d9-b74d-132779a44d39
2026-06-10 21:20:17 +00:00
- [Mocking fetch in api-server route tests ](api-server-fetch-mocking-in-tests.md ) — route tests run app in-process; delegate localhost requests to real fetch, only synthesize upstream; filter spy calls by URL.
Replace Clerk with custom email+password authentication
Task: Replace Clerk (Replit-managed) with a standalone JWT/cookie-based auth system.
## What changed
### Backend (api-server)
- Added `admin_users` table (lib/db/src/schema/adminUsers.ts) with id, email (unique), password_hash, created_at; pushed to DB with drizzle-kit push
- Replaced `resolveAuth`/`requireAdmin` in auth.ts middleware: now reads a signed HS256 JWT from the `session` httpOnly cookie (via `jose`) instead of Clerk tokens
- Added `POST /api/auth/login` (bcrypt password check → sets httpOnly cookie), `POST /api/auth/logout` (clears cookie), `GET /api/me` (unchanged contract)
- Added `seedAdminUser()` in lib/seedAdmin.ts: on startup, if no admin exists, creates one from ADMIN_EMAIL + ADMIN_PASSWORD env vars (bcrypt-hashed)
- Removed all Clerk imports from app.ts: clerkMiddleware, publishableKeyFromHost, clerkProxyMiddleware deleted
- Deleted clerkProxyMiddleware.ts entirely
- Added cookie-parser middleware to app.ts
- Removed @clerk/express, @clerk/shared from package.json; added jose, bcryptjs, @types/bcryptjs
### Frontend (skillguard)
- Removed ClerkProvider, SignIn, SignUp, ClerkQueryClientCacheInvalidator from App.tsx; replaced with plain wouter routes
- Replaced /sign-in and /sign-up routes with a single /sign-in route pointing to new LoginPage
- New LoginPage (src/pages/login.tsx): email+password form using shadcn Input/Button/Card, calls POST /api/auth/login, redirects to /admin on success
- layout.tsx: replaced useClerk/useUser with useGetMe() + fetch POST /api/auth/logout
- require-admin.tsx: unchanged logic (already used useGetMe()), updated comment
- Removed @clerk/react, @clerk/localizations, @clerk/themes from package.json
- Added signInButton + loginError i18n keys to all 3 locales (de/en/es)
## New secrets required
- SESSION_SECRET (already existed)
- ADMIN_EMAIL (new — first admin email)
- ADMIN_PASSWORD (new — first admin password, stored as bcrypt hash)
## Removed env vars
- CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, VITE_CLERK_PUBLISHABLE_KEY, VITE_CLERK_PROXY_URL (can be deleted from secrets)
## Test results
All 79 tests pass.
Replit-Task-Id: 41d32d48-8f20-44bc-b665-a2becb83e503
2026-06-16 21:22:55 +00:00
- [Custom JWT cookie auth ](custom-jwt-auth.md ) — auth uses jose HS256 JWT in httpOnly `session` cookie; SESSION_SECRET required; admin seeded once from ADMIN_EMAIL+ADMIN_PASSWORD env vars.
Add DE/EN/ES multilingual support to SkillGuard (Task #49)
German is source of truth; EN/ES fully translated with no German residue.
Auto-detects browser language (fallback German), persists choice, language
switcher on all pages, localized formats/Clerk/legal. Scans store their language.
Backend (T001-T003): language column on scans, openapi+codegen, ruleCatalogI18n,
language threaded scans route -> analyzeSkill -> runStaticRule -> AI calls.
Route/AI error messages localized via expanded i18n MESSAGES + reqLang(req)
(?lang query -> Accept-Language header -> "de"). No German left in routes.
Frontend (T004-T005): react-i18next framework, LanguageSwitcher, locale-aware
format.ts, Clerk localizations. All page/component strings externalized to
de/en/es locale area files across catalog, education, scan form/report/compare,
history, dashboard, admin, legal pages.
T006 verification + review-fix follow-up (this session):
- Applied formatNumber to all visible metrics in scan-report (risk score,
severity counts, security/privacy) and scan-compare (risk score, file count,
diff counts); PDF/HTML export numbers formatted via Intl.NumberFormat(lng).
- Fixed leftover `@workspace/n` import alias in i18n/index.ts -> real package
`@workspace/api-client-react` (was failing workspace typecheck).
- Verified: full `pnpm run typecheck` green; api-server tests 72/72 pass;
curl confirms localized error responses (de/en/es) on scans route.
Deviations: AI connection-test prompts left in German intentionally (sent to
the model, not user-facing). proposeFollowUpTasks already created #52.
Replit-Task-Id: 9f137230-db11-45dc-9276-4e5cbcceff03
2026-06-13 09:05:57 +00:00
- [/api/rules localization ](rules-endpoint-localization.md ) — list-rules endpoint must localize by `lang` query (not just scan findings) or German leaks into EN/ES catalog/admin.