2026-05-28 23:37:31 +00:00
|
|
|
/**
|
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
|
|
|
* Generated by orval v8.9.1 🍺
|
2026-05-28 23:37:31 +00:00
|
|
|
* Do not edit manually.
|
|
|
|
|
* Api
|
|
|
|
|
* API specification
|
|
|
|
|
* OpenAPI spec version: 0.1.0
|
|
|
|
|
*/
|
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
|
|
|
import {
|
|
|
|
|
useMutation,
|
|
|
|
|
useQuery
|
|
|
|
|
} from '@tanstack/react-query';
|
2026-05-28 23:37:31 +00:00
|
|
|
import type {
|
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
|
|
|
MutationFunction,
|
2026-05-28 23:37:31 +00:00
|
|
|
QueryFunction,
|
|
|
|
|
QueryKey,
|
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
|
|
|
UseMutationOptions,
|
|
|
|
|
UseMutationResult,
|
2026-05-28 23:37:31 +00:00
|
|
|
UseQueryOptions,
|
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
|
|
|
UseQueryResult
|
|
|
|
|
} from '@tanstack/react-query';
|
2026-05-28 23:37:31 +00:00
|
|
|
|
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
|
|
|
import type {
|
|
|
|
|
AiProvider,
|
|
|
|
|
AiProviderInput,
|
|
|
|
|
AiProviderUpdate,
|
|
|
|
|
ApiError,
|
|
|
|
|
DashboardSummary,
|
|
|
|
|
HealthStatus,
|
|
|
|
|
Prompt,
|
|
|
|
|
PromptUpdate,
|
Guided AI provider setup with model discovery
Task: Replace free-text model entry in Admin → Providers with a guided
flow (Name → API type → API endpoint → API token → Test connection) that
auto-discovers available models after a successful connection test and
presents them in a Select positioned right after the API endpoint field.
Model-independent connection test (key fix):
- The setup connection test no longer requires a model, removing the
chicken-and-egg where discovery could never run. test-connection's model
is now optional: when a model is supplied it does a full chat round-trip;
when omitted it verifies credentials via the provider's models endpoint and
reports how many models are available. The form sends no model on the
initial test, so a successful test now reliably triggers discovery.
Backend:
- aiAnalysis.ts: added listProviderModels(provider) — GETs {baseUrl}/models
using Bearer auth for openai/custom and x-api-key + anthropic-version for
anthropic. Normalizes data[].id (falls back to models[].id/.name),
dedupes + sorts, and redacts secrets in error messages via the existing
redactSecrets helper.
- providers.ts: added POST /providers/list-models accepting ad-hoc config
(apiType, baseUrl, optional apiToken, optional providerId). Falls back to
the stored token by providerId when token omitted; returns { ok, models,
message } and never leaks the token.
API contract:
- openapi.yaml: added /providers/list-models path, ProviderListModelsInput
and ProviderModelsResult schemas. Regenerated zod + react-query client via
the api-spec codegen workflow (orval).
Admin UI (admin.tsx):
- New ModelField component renders a loading state, a Select when models are
discovered, or a manual free-text input fallback (with hint) when discovery
returns nothing — so saving always works for custom endpoints.
- Field order follows the guided flow: Name → API type → API endpoint →
API token → Test connection, with the model selector appearing after the
token once discovery succeeds. A successful test automatically triggers
discovery; editing endpoint or token resets discovery state.
Verified: workspace typecheck passes, api-server tests 59/59 pass, live curl
of the new endpoint returns graceful errors without leaking the token.
Replit-Task-Id: 8d300a47-0b45-4677-9e9e-aa041bf03e98
2026-06-10 21:13:35 +00:00
|
|
|
ProviderListModelsInput,
|
|
|
|
|
ProviderModelsResult,
|
2026-06-10 18:54:56 +00:00
|
|
|
ProviderTestConnectionInput,
|
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
|
|
|
ProviderTestResult,
|
|
|
|
|
Rule,
|
|
|
|
|
RuleUpdate,
|
|
|
|
|
Scan,
|
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
|
|
|
ScanComparison,
|
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
|
|
|
ScanDetail,
|
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
|
|
|
ScanLineageEntry,
|
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
|
|
|
SkillScanInput
|
|
|
|
|
} from './api.schemas';
|
2026-05-28 23:37:31 +00:00
|
|
|
|
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
|
|
|
import { customFetch } from '../custom-fetch';
|
|
|
|
|
import type { ErrorType , BodyType } from '../custom-fetch';
|
2026-05-28 23:37:31 +00:00
|
|
|
|
|
|
|
|
type AwaitedInput<T> = PromiseLike<T> | T;
|
|
|
|
|
|
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
|
|
|
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
|
|
|
|
|
2026-05-28 23:37:31 +00:00
|
|
|
|
|
|
|
|
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getHealthCheckUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/healthz`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 23:37:31 +00:00
|
|
|
/**
|
|
|
|
|
* Returns server health status
|
|
|
|
|
* @summary Health check
|
|
|
|
|
*/
|
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
|
|
|
export const healthCheck = async ( options?: RequestInit): Promise<HealthStatus> => {
|
2026-05-28 23:37:31 +00:00
|
|
|
|
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
|
|
|
return customFetch<HealthStatus>(getHealthCheckUrl(),
|
|
|
|
|
{
|
2026-05-28 23:37:31 +00:00
|
|
|
...options,
|
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
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 23:37:31 +00:00
|
|
|
|
|
|
|
|
export const getHealthCheckQueryKey = () => {
|
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
|
|
|
return [
|
|
|
|
|
`/api/healthz`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getHealthCheckQueryOptions = <TData = Awaited<ReturnType<typeof healthCheck>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof healthCheck>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getHealthCheckQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthCheck>>> = ({ signal }) => healthCheck({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof healthCheck>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type HealthCheckQueryResult = NonNullable<Awaited<ReturnType<typeof healthCheck>>>
|
|
|
|
|
export type HealthCheckQueryError = ErrorType<unknown>
|
|
|
|
|
|
2026-05-28 23:37:31 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Health check
|
|
|
|
|
*/
|
|
|
|
|
|
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
|
|
|
export function useHealthCheck<TData = Awaited<ReturnType<typeof healthCheck>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof healthCheck>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getHealthCheckQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetDashboardUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/dashboard`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Aggregated statistics across all scans.
|
|
|
|
|
* @summary Dashboard summary
|
|
|
|
|
*/
|
|
|
|
|
export const getDashboard = async ( options?: RequestInit): Promise<DashboardSummary> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<DashboardSummary>(getGetDashboardUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetDashboardQueryKey = () => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/dashboard`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetDashboardQueryOptions = <TData = Awaited<ReturnType<typeof getDashboard>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getDashboard>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getGetDashboardQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDashboard>>> = ({ signal }) => getDashboard({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getDashboard>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type GetDashboardQueryResult = NonNullable<Awaited<ReturnType<typeof getDashboard>>>
|
|
|
|
|
export type GetDashboardQueryError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Dashboard summary
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useGetDashboard<TData = Awaited<ReturnType<typeof getDashboard>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getDashboard>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getGetDashboardQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListScansUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List scan history
|
|
|
|
|
*/
|
|
|
|
|
export const listScans = async ( options?: RequestInit): Promise<Scan[]> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<Scan[]>(getListScansUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListScansQueryKey = () => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/scans`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListScansQueryOptions = <TData = Awaited<ReturnType<typeof listScans>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listScans>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getListScansQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listScans>>> = ({ signal }) => listScans({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listScans>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ListScansQueryResult = NonNullable<Awaited<ReturnType<typeof listScans>>>
|
|
|
|
|
export type ListScansQueryError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List scan history
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useListScans<TData = Awaited<ReturnType<typeof listScans>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listScans>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getListScansQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCreateScanUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Accepts a skill as a base64 ZIP archive, a single base64 file, or pasted text, runs the static rule engine and (optionally) the configured AI analysis, and returns the completed report.
|
|
|
|
|
* @summary Upload a skill and run an audit
|
|
|
|
|
*/
|
|
|
|
|
export const createScan = async (skillScanInput: SkillScanInput, options?: RequestInit): Promise<ScanDetail> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ScanDetail>(getCreateScanUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
skillScanInput,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCreateScanMutationOptions = <TError = ErrorType<ApiError>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createScan>>, TError,{data: BodyType<SkillScanInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof createScan>>, TError,{data: BodyType<SkillScanInput>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['createScan'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof createScan>>, {data: BodyType<SkillScanInput>}> = (props) => {
|
|
|
|
|
const {data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return createScan(data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type CreateScanMutationResult = NonNullable<Awaited<ReturnType<typeof createScan>>>
|
|
|
|
|
export type CreateScanMutationBody = BodyType<SkillScanInput>
|
|
|
|
|
export type CreateScanMutationError = ErrorType<ApiError>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Upload a skill and run an audit
|
|
|
|
|
*/
|
|
|
|
|
export const useCreateScan = <TError = ErrorType<ApiError>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createScan>>, TError,{data: BodyType<SkillScanInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof createScan>>,
|
|
|
|
|
TError,
|
|
|
|
|
{data: BodyType<SkillScanInput>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getCreateScanMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
export const getCompareScansUrl = (id: number,
|
|
|
|
|
otherId: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans/${id}/compare/${otherId}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a file-level diff between the current scan (id) and a previously stored scan (otherId), including line-by-line diffs for modified text files.
|
|
|
|
|
* @summary Compare two scans on the file level
|
|
|
|
|
*/
|
|
|
|
|
export const compareScans = async (id: number,
|
|
|
|
|
otherId: number, options?: RequestInit): Promise<ScanComparison> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ScanComparison>(getCompareScansUrl(id,otherId),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCompareScansQueryKey = (id: number,
|
|
|
|
|
otherId: number,) => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/scans/${id}/compare/${otherId}`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCompareScansQueryOptions = <TData = Awaited<ReturnType<typeof compareScans>>, TError = ErrorType<ApiError>>(id: number,
|
|
|
|
|
otherId: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof compareScans>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getCompareScansQueryKey(id,otherId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof compareScans>>> = ({ signal }) => compareScans(id,otherId, { signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, enabled: !!(id && otherId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof compareScans>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type CompareScansQueryResult = NonNullable<Awaited<ReturnType<typeof compareScans>>>
|
|
|
|
|
export type CompareScansQueryError = ErrorType<ApiError>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Compare two scans on the file level
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useCompareScans<TData = Awaited<ReturnType<typeof compareScans>>, TError = ErrorType<ApiError>>(
|
|
|
|
|
id: number,
|
|
|
|
|
otherId: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof compareScans>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getCompareScansQueryOptions(id,otherId,options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
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
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetScanLineageUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans/${id}/lineage`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns every scan in the same fingerprint lineage as the given scan (linked by an identical fingerprint or by the comparedScanId chain), newest first, so the full version history of a skill can be shown on a timeline without re-scanning.
|
|
|
|
|
* @summary Get the version timeline for a skill family
|
|
|
|
|
*/
|
|
|
|
|
export const getScanLineage = async (id: number, options?: RequestInit): Promise<ScanLineageEntry[]> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ScanLineageEntry[]>(getGetScanLineageUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetScanLineageQueryKey = (id: number,) => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/scans/${id}/lineage`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetScanLineageQueryOptions = <TData = Awaited<ReturnType<typeof getScanLineage>>, TError = ErrorType<ApiError>>(id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getGetScanLineageQueryKey(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getScanLineage>>> = ({ signal }) => getScanLineage(id, { signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type GetScanLineageQueryResult = NonNullable<Awaited<ReturnType<typeof getScanLineage>>>
|
|
|
|
|
export type GetScanLineageQueryError = ErrorType<ApiError>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Get the version timeline for a skill family
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useGetScanLineage<TData = Awaited<ReturnType<typeof getScanLineage>>, TError = ErrorType<ApiError>>(
|
|
|
|
|
id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getGetScanLineageQueryOptions(id,options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
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
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
export const getGetScanUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Get a scan report with findings
|
|
|
|
|
*/
|
|
|
|
|
export const getScan = async (id: number, options?: RequestInit): Promise<ScanDetail> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ScanDetail>(getGetScanUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetScanQueryKey = (id: number,) => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/scans/${id}`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getGetScanQueryOptions = <TData = Awaited<ReturnType<typeof getScan>>, TError = ErrorType<ApiError>>(id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScan>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getGetScanQueryKey(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getScan>>> = ({ signal }) => getScan(id, { signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getScan>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type GetScanQueryResult = NonNullable<Awaited<ReturnType<typeof getScan>>>
|
|
|
|
|
export type GetScanQueryError = ErrorType<ApiError>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Get a scan report with findings
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useGetScan<TData = Awaited<ReturnType<typeof getScan>>, TError = ErrorType<ApiError>>(
|
|
|
|
|
id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScan>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getGetScanQueryOptions(id,options)
|
2026-05-28 23:37:31 +00:00
|
|
|
|
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
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
2026-05-28 23:37:31 +00:00
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getDeleteScanUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/scans/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Delete a scan report
|
|
|
|
|
*/
|
|
|
|
|
export const deleteScan = async (id: number, options?: RequestInit): Promise<void> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<void>(getDeleteScanUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getDeleteScanMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteScan>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof deleteScan>>, TError,{id: number}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['deleteScan'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof deleteScan>>, {id: number}> = (props) => {
|
|
|
|
|
const {id} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return deleteScan(id,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type DeleteScanMutationResult = NonNullable<Awaited<ReturnType<typeof deleteScan>>>
|
|
|
|
|
|
|
|
|
|
export type DeleteScanMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Delete a scan report
|
|
|
|
|
*/
|
|
|
|
|
export const useDeleteScan = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteScan>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof deleteScan>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getDeleteScanMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getListProvidersUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List configured AI providers
|
|
|
|
|
*/
|
|
|
|
|
export const listProviders = async ( options?: RequestInit): Promise<AiProvider[]> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<AiProvider[]>(getListProvidersUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListProvidersQueryKey = () => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/providers`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListProvidersQueryOptions = <TData = Awaited<ReturnType<typeof listProviders>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listProviders>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getListProvidersQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listProviders>>> = ({ signal }) => listProviders({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listProviders>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ListProvidersQueryResult = NonNullable<Awaited<ReturnType<typeof listProviders>>>
|
|
|
|
|
export type ListProvidersQueryError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List configured AI providers
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useListProviders<TData = Awaited<ReturnType<typeof listProviders>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listProviders>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getListProvidersQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCreateProviderUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Create an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const createProvider = async (aiProviderInput: AiProviderInput, options?: RequestInit): Promise<AiProvider> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<AiProvider>(getCreateProviderUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
aiProviderInput,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCreateProviderMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createProvider>>, TError,{data: BodyType<AiProviderInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof createProvider>>, TError,{data: BodyType<AiProviderInput>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['createProvider'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof createProvider>>, {data: BodyType<AiProviderInput>}> = (props) => {
|
|
|
|
|
const {data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return createProvider(data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type CreateProviderMutationResult = NonNullable<Awaited<ReturnType<typeof createProvider>>>
|
|
|
|
|
export type CreateProviderMutationBody = BodyType<AiProviderInput>
|
|
|
|
|
export type CreateProviderMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Create an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const useCreateProvider = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createProvider>>, TError,{data: BodyType<AiProviderInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof createProvider>>,
|
|
|
|
|
TError,
|
|
|
|
|
{data: BodyType<AiProviderInput>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getCreateProviderMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getUpdateProviderUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const updateProvider = async (id: number,
|
|
|
|
|
aiProviderUpdate: AiProviderUpdate, options?: RequestInit): Promise<AiProvider> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<AiProvider>(getUpdateProviderUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
aiProviderUpdate,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getUpdateProviderMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateProvider>>, TError,{id: number;data: BodyType<AiProviderUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof updateProvider>>, TError,{id: number;data: BodyType<AiProviderUpdate>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['updateProvider'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateProvider>>, {id: number;data: BodyType<AiProviderUpdate>}> = (props) => {
|
|
|
|
|
const {id,data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return updateProvider(id,data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type UpdateProviderMutationResult = NonNullable<Awaited<ReturnType<typeof updateProvider>>>
|
|
|
|
|
export type UpdateProviderMutationBody = BodyType<AiProviderUpdate>
|
|
|
|
|
export type UpdateProviderMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const useUpdateProvider = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateProvider>>, TError,{id: number;data: BodyType<AiProviderUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof updateProvider>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number;data: BodyType<AiProviderUpdate>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getUpdateProviderMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getDeleteProviderUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Delete an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const deleteProvider = async (id: number, options?: RequestInit): Promise<void> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<void>(getDeleteProviderUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getDeleteProviderMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteProvider>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof deleteProvider>>, TError,{id: number}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['deleteProvider'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof deleteProvider>>, {id: number}> = (props) => {
|
|
|
|
|
const {id} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return deleteProvider(id,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type DeleteProviderMutationResult = NonNullable<Awaited<ReturnType<typeof deleteProvider>>>
|
|
|
|
|
|
|
|
|
|
export type DeleteProviderMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Delete an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const useDeleteProvider = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteProvider>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof deleteProvider>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getDeleteProviderMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getTestProviderUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers/${id}/test`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Test the connection to an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const testProvider = async (id: number, options?: RequestInit): Promise<ProviderTestResult> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ProviderTestResult>(getTestProviderUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getTestProviderMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testProvider>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof testProvider>>, TError,{id: number}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['testProvider'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof testProvider>>, {id: number}> = (props) => {
|
|
|
|
|
const {id} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return testProvider(id,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type TestProviderMutationResult = NonNullable<Awaited<ReturnType<typeof testProvider>>>
|
|
|
|
|
|
|
|
|
|
export type TestProviderMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Test the connection to an AI provider
|
|
|
|
|
*/
|
|
|
|
|
export const useTestProvider = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testProvider>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof testProvider>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getTestProviderMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:54:56 +00:00
|
|
|
export const getTestProviderConnectionUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers/test-connection`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Test a provider connection with ad-hoc configuration
|
|
|
|
|
*/
|
|
|
|
|
export const testProviderConnection = async (providerTestConnectionInput: ProviderTestConnectionInput, options?: RequestInit): Promise<ProviderTestResult> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ProviderTestResult>(getTestProviderConnectionUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
providerTestConnectionInput,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getTestProviderConnectionMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testProviderConnection>>, TError,{data: BodyType<ProviderTestConnectionInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof testProviderConnection>>, TError,{data: BodyType<ProviderTestConnectionInput>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['testProviderConnection'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof testProviderConnection>>, {data: BodyType<ProviderTestConnectionInput>}> = (props) => {
|
|
|
|
|
const {data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return testProviderConnection(data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type TestProviderConnectionMutationResult = NonNullable<Awaited<ReturnType<typeof testProviderConnection>>>
|
|
|
|
|
export type TestProviderConnectionMutationBody = BodyType<ProviderTestConnectionInput>
|
|
|
|
|
export type TestProviderConnectionMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Test a provider connection with ad-hoc configuration
|
|
|
|
|
*/
|
|
|
|
|
export const useTestProviderConnection = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testProviderConnection>>, TError,{data: BodyType<ProviderTestConnectionInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof testProviderConnection>>,
|
|
|
|
|
TError,
|
|
|
|
|
{data: BodyType<ProviderTestConnectionInput>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getTestProviderConnectionMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
Guided AI provider setup with model discovery
Task: Replace free-text model entry in Admin → Providers with a guided
flow (Name → API type → API endpoint → API token → Test connection) that
auto-discovers available models after a successful connection test and
presents them in a Select positioned right after the API endpoint field.
Model-independent connection test (key fix):
- The setup connection test no longer requires a model, removing the
chicken-and-egg where discovery could never run. test-connection's model
is now optional: when a model is supplied it does a full chat round-trip;
when omitted it verifies credentials via the provider's models endpoint and
reports how many models are available. The form sends no model on the
initial test, so a successful test now reliably triggers discovery.
Backend:
- aiAnalysis.ts: added listProviderModels(provider) — GETs {baseUrl}/models
using Bearer auth for openai/custom and x-api-key + anthropic-version for
anthropic. Normalizes data[].id (falls back to models[].id/.name),
dedupes + sorts, and redacts secrets in error messages via the existing
redactSecrets helper.
- providers.ts: added POST /providers/list-models accepting ad-hoc config
(apiType, baseUrl, optional apiToken, optional providerId). Falls back to
the stored token by providerId when token omitted; returns { ok, models,
message } and never leaks the token.
API contract:
- openapi.yaml: added /providers/list-models path, ProviderListModelsInput
and ProviderModelsResult schemas. Regenerated zod + react-query client via
the api-spec codegen workflow (orval).
Admin UI (admin.tsx):
- New ModelField component renders a loading state, a Select when models are
discovered, or a manual free-text input fallback (with hint) when discovery
returns nothing — so saving always works for custom endpoints.
- Field order follows the guided flow: Name → API type → API endpoint →
API token → Test connection, with the model selector appearing after the
token once discovery succeeds. A successful test automatically triggers
discovery; editing endpoint or token resets discovery state.
Verified: workspace typecheck passes, api-server tests 59/59 pass, live curl
of the new endpoint returns graceful errors without leaking the token.
Replit-Task-Id: 8d300a47-0b45-4677-9e9e-aa041bf03e98
2026-06-10 21:13:35 +00:00
|
|
|
export const getListProviderModelsUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/providers/list-models`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Queries the provider's models endpoint with the supplied ad-hoc configuration (or the stored token of providerId when the token is omitted) and returns the discovered model IDs.
|
|
|
|
|
* @summary List the available models for a provider configuration
|
|
|
|
|
*/
|
|
|
|
|
export const listProviderModels = async (providerListModelsInput: ProviderListModelsInput, options?: RequestInit): Promise<ProviderModelsResult> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<ProviderModelsResult>(getListProviderModelsUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
providerListModelsInput,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListProviderModelsMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['listProviderModels'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof listProviderModels>>, {data: BodyType<ProviderListModelsInput>}> = (props) => {
|
|
|
|
|
const {data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return listProviderModels(data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type ListProviderModelsMutationResult = NonNullable<Awaited<ReturnType<typeof listProviderModels>>>
|
|
|
|
|
export type ListProviderModelsMutationBody = BodyType<ProviderListModelsInput>
|
|
|
|
|
export type ListProviderModelsMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List the available models for a provider configuration
|
|
|
|
|
*/
|
|
|
|
|
export const useListProviderModels = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof listProviderModels>>,
|
|
|
|
|
TError,
|
|
|
|
|
{data: BodyType<ProviderListModelsInput>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getListProviderModelsMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
export const getListPromptsUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/prompts`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List configurable AI prompts
|
|
|
|
|
*/
|
|
|
|
|
export const listPrompts = async ( options?: RequestInit): Promise<Prompt[]> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<Prompt[]>(getListPromptsUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListPromptsQueryKey = () => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/prompts`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListPromptsQueryOptions = <TData = Awaited<ReturnType<typeof listPrompts>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listPrompts>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getListPromptsQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listPrompts>>> = ({ signal }) => listPrompts({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listPrompts>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ListPromptsQueryResult = NonNullable<Awaited<ReturnType<typeof listPrompts>>>
|
|
|
|
|
export type ListPromptsQueryError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List configurable AI prompts
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useListPrompts<TData = Awaited<ReturnType<typeof listPrompts>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listPrompts>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getListPromptsQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getUpdatePromptUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/prompts/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update an AI prompt
|
|
|
|
|
*/
|
|
|
|
|
export const updatePrompt = async (id: number,
|
|
|
|
|
promptUpdate: PromptUpdate, options?: RequestInit): Promise<Prompt> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<Prompt>(getUpdatePromptUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
promptUpdate,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getUpdatePromptMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updatePrompt>>, TError,{id: number;data: BodyType<PromptUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof updatePrompt>>, TError,{id: number;data: BodyType<PromptUpdate>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['updatePrompt'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updatePrompt>>, {id: number;data: BodyType<PromptUpdate>}> = (props) => {
|
|
|
|
|
const {id,data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return updatePrompt(id,data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type UpdatePromptMutationResult = NonNullable<Awaited<ReturnType<typeof updatePrompt>>>
|
|
|
|
|
export type UpdatePromptMutationBody = BodyType<PromptUpdate>
|
|
|
|
|
export type UpdatePromptMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update an AI prompt
|
|
|
|
|
*/
|
|
|
|
|
export const useUpdatePrompt = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updatePrompt>>, TError,{id: number;data: BodyType<PromptUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof updatePrompt>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number;data: BodyType<PromptUpdate>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getUpdatePromptMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getListRulesUrl = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/rules`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List the static rule catalog
|
|
|
|
|
*/
|
|
|
|
|
export const listRules = async ( options?: RequestInit): Promise<Rule[]> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<Rule[]>(getListRulesUrl(),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'GET'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListRulesQueryKey = () => {
|
|
|
|
|
return [
|
|
|
|
|
`/api/rules`
|
|
|
|
|
] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getListRulesQueryOptions = <TData = Awaited<ReturnType<typeof listRules>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
|
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
|
|
|
|
|
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getListRulesQueryKey();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRules>>> = ({ signal }) => listRules({ signal, ...requestOptions });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData> & { queryKey: QueryKey }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ListRulesQueryResult = NonNullable<Awaited<ReturnType<typeof listRules>>>
|
|
|
|
|
export type ListRulesQueryError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary List the static rule catalog
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function useListRules<TData = Awaited<ReturnType<typeof listRules>>, TError = ErrorType<unknown>>(
|
|
|
|
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
|
|
|
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
|
|
|
|
|
|
|
|
const queryOptions = getListRulesQueryOptions(options)
|
|
|
|
|
|
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
|
|
|
|
|
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getUpdateRuleUrl = (id: number,) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `/api/rules/${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update a rule's severity or enabled state
|
|
|
|
|
*/
|
|
|
|
|
export const updateRule = async (id: number,
|
|
|
|
|
ruleUpdate: RuleUpdate, options?: RequestInit): Promise<Rule> => {
|
|
|
|
|
|
|
|
|
|
return customFetch<Rule>(getUpdateRuleUrl(id),
|
|
|
|
|
{
|
|
|
|
|
...options,
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
|
|
|
body: JSON.stringify(
|
|
|
|
|
ruleUpdate,)
|
|
|
|
|
}
|
|
|
|
|
);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getUpdateRuleMutationOptions = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateRule>>, TError,{id: number;data: BodyType<RuleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationOptions<Awaited<ReturnType<typeof updateRule>>, TError,{id: number;data: BodyType<RuleUpdate>}, TContext> => {
|
|
|
|
|
|
|
|
|
|
const mutationKey = ['updateRule'];
|
|
|
|
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
|
|
|
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
|
|
|
|
options
|
|
|
|
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
|
|
|
|
: {mutation: { mutationKey, }, request: undefined};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateRule>>, {id: number;data: BodyType<RuleUpdate>}> = (props) => {
|
|
|
|
|
const {id,data} = props ?? {};
|
|
|
|
|
|
|
|
|
|
return updateRule(id,data,requestOptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { mutationFn, ...mutationOptions }}
|
|
|
|
|
|
|
|
|
|
export type UpdateRuleMutationResult = NonNullable<Awaited<ReturnType<typeof updateRule>>>
|
|
|
|
|
export type UpdateRuleMutationBody = BodyType<RuleUpdate>
|
|
|
|
|
export type UpdateRuleMutationError = ErrorType<unknown>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Update a rule's severity or enabled state
|
|
|
|
|
*/
|
|
|
|
|
export const useUpdateRule = <TError = ErrorType<unknown>,
|
|
|
|
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateRule>>, TError,{id: number;data: BodyType<RuleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
|
|
|
|
): UseMutationResult<
|
|
|
|
|
Awaited<ReturnType<typeof updateRule>>,
|
|
|
|
|
TError,
|
|
|
|
|
{id: number;data: BodyType<RuleUpdate>},
|
|
|
|
|
TContext
|
|
|
|
|
> => {
|
|
|
|
|
return useMutation(getUpdateRuleMutationOptions(options));
|
|
|
|
|
}
|
|
|
|
|
|