skillguard/lib/api-spec/openapi.yaml
amertensreplit 2236ad179d Add DE/EN/ES multilingual support to SkillGuard (Task #49)
German is source of truth; EN/ES fully translated with no German residue.
Auto-detects browser language (fallback German), persists choice, language
switcher on all pages, localized formats/Clerk/legal. Scans store their language.

Backend (T001-T003): language column on scans, openapi+codegen, ruleCatalogI18n,
language threaded scans route -> analyzeSkill -> runStaticRule -> AI calls.
Route/AI error messages localized via expanded i18n MESSAGES + reqLang(req)
(?lang query -> Accept-Language header -> "de"). No German left in routes.

Frontend (T004-T005): react-i18next framework, LanguageSwitcher, locale-aware
format.ts, Clerk localizations. All page/component strings externalized to
de/en/es locale area files across catalog, education, scan form/report/compare,
history, dashboard, admin, legal pages.

T006 verification + review-fix follow-up (this session):
- Applied formatNumber to all visible metrics in scan-report (risk score,
  severity counts, security/privacy) and scan-compare (risk score, file count,
  diff counts); PDF/HTML export numbers formatted via Intl.NumberFormat(lng).
- Fixed leftover `@workspace/n` import alias in i18n/index.ts -> real package
  `@workspace/api-client-react` (was failing workspace typecheck).
- Verified: full `pnpm run typecheck` green; api-server tests 72/72 pass;
  curl confirms localized error responses (de/en/es) on scans route.

Deviations: AI connection-test prompts left in German intentionally (sent to
the model, not user-facing). proposeFollowUpTasks already created #52.

Replit-Task-Id: 9f137230-db11-45dc-9276-4e5cbcceff03
2026-06-13 09:05:57 +00:00

1184 lines
30 KiB
YAML

openapi: 3.1.0
info:
# Do not change the title, if the title changes, the import paths will be broken
title: Api
version: 0.1.0
description: API specification
servers:
- url: /api
description: Base API path
tags:
- name: health
description: Health operations
- name: auth
description: Authentication status
- name: scans
description: Skill scans and audit reports
- name: providers
description: Configurable external AI providers
- name: prompts
description: Configurable AI analysis prompts
- name: rules
description: Static rule catalog configuration
- name: dashboard
description: Dashboard summaries
paths:
/healthz:
get:
operationId: healthCheck
tags: [health]
summary: Health check
description: Returns server health status
responses:
"200":
description: Healthy
content:
application/json:
schema:
$ref: "#/components/schemas/HealthStatus"
/me:
get:
operationId: getMe
tags: [auth]
summary: Current authentication and admin status
description: >-
Returns whether the caller is signed in and whether their email is on
the admin allowlist. Always 200; never requires auth.
responses:
"200":
description: Authentication status
content:
application/json:
schema:
$ref: "#/components/schemas/AuthMe"
/dashboard:
get:
operationId: getDashboard
tags: [dashboard]
summary: Dashboard summary
description: Aggregated statistics across all scans.
responses:
"200":
description: Dashboard summary
content:
application/json:
schema:
$ref: "#/components/schemas/DashboardSummary"
/scans:
get:
operationId: listScans
tags: [scans]
summary: List scan history
responses:
"200":
description: List of scans (most recent first)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Scan"
post:
operationId: createScan
tags: [scans]
summary: Upload a skill and run an audit
description: >-
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.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SkillScanInput"
responses:
"201":
description: Completed scan report
content:
application/json:
schema:
$ref: "#/components/schemas/ScanDetail"
"400":
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
/scans/{id}/compare/{otherId}:
get:
operationId: compareScans
tags: [scans]
summary: Compare two scans on the file level
description: >-
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.
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: otherId
in: path
required: true
schema:
type: integer
responses:
"200":
description: File-level comparison
content:
application/json:
schema:
$ref: "#/components/schemas/ScanComparison"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
/scans/{id}/lineage:
get:
operationId: getScanLineage
tags: [scans]
summary: Get the version timeline for a skill family
description: >-
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.
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Version timeline (most recent first)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ScanLineageEntry"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
/scans/{id}:
get:
operationId: getScan
tags: [scans]
summary: Get a scan report with findings
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Scan report
content:
application/json:
schema:
$ref: "#/components/schemas/ScanDetail"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
patch:
operationId: moderateScan
tags: [scans]
summary: Moderate a scan (hide or unhide from the public catalog)
description: Admin-only. Toggles whether a scan appears in the public catalog.
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScanModerationUpdate"
responses:
"200":
description: Updated scan
content:
application/json:
schema:
$ref: "#/components/schemas/Scan"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
delete:
operationId: deleteScan
tags: [scans]
summary: Delete a scan report
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Deleted
/scans/{id}/description:
post:
operationId: generateScanDescription
tags: [scans]
summary: Generate the AI description for an existing scan
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Scan report with the newly generated description
content:
application/json:
schema:
$ref: "#/components/schemas/ScanDetail"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
"422":
description: Description could not be generated
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
/providers:
get:
operationId: listProviders
tags: [providers]
summary: List configured AI providers
responses:
"200":
description: List of providers (tokens masked)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AiProvider"
post:
operationId: createProvider
tags: [providers]
summary: Create an AI provider
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AiProviderInput"
responses:
"201":
description: Created provider
content:
application/json:
schema:
$ref: "#/components/schemas/AiProvider"
/providers/{id}:
patch:
operationId: updateProvider
tags: [providers]
summary: Update an AI provider
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AiProviderUpdate"
responses:
"200":
description: Updated provider
content:
application/json:
schema:
$ref: "#/components/schemas/AiProvider"
delete:
operationId: deleteProvider
tags: [providers]
summary: Delete an AI provider
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Deleted
/providers/{id}/test:
post:
operationId: testProvider
tags: [providers]
summary: Test the connection to an AI provider
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Test result
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderTestResult"
/providers/test-connection:
post:
operationId: testProviderConnection
tags: [providers]
summary: Test a provider connection with ad-hoc configuration
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderTestConnectionInput"
responses:
"200":
description: Test result
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderTestResult"
/providers/list-models:
post:
operationId: listProviderModels
tags: [providers]
summary: List the available models for a provider configuration
description: >-
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.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderListModelsInput"
responses:
"200":
description: Discovered models
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderModelsResult"
/prompts:
get:
operationId: listPrompts
tags: [prompts]
summary: List configurable AI prompts
responses:
"200":
description: List of prompts
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Prompt"
/prompts/{id}:
patch:
operationId: updatePrompt
tags: [prompts]
summary: Update an AI prompt
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PromptUpdate"
responses:
"200":
description: Updated prompt
content:
application/json:
schema:
$ref: "#/components/schemas/Prompt"
/rules:
get:
operationId: listRules
tags: [rules]
summary: List the static rule catalog
parameters:
- name: lang
in: query
required: false
schema:
type: string
enum: [de, en, es]
description: Language for the rule catalog text (title/description/category). Defaults to "de".
responses:
"200":
description: List of rules
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Rule"
/rules/{id}:
patch:
operationId: updateRule
tags: [rules]
summary: Update a rule's severity or enabled state
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RuleUpdate"
responses:
"200":
description: Updated rule
content:
application/json:
schema:
$ref: "#/components/schemas/Rule"
components:
schemas:
HealthStatus:
type: object
properties:
status:
type: string
required:
- status
ApiError:
type: object
required: [error]
properties:
error:
type: string
AuthMe:
type: object
required: [authenticated, isAdmin]
properties:
authenticated:
type: boolean
isAdmin:
type: boolean
email:
type: ["string", "null"]
description: The signed-in user's primary email, when available
ScanModerationUpdate:
type: object
required: [hidden]
properties:
hidden:
type: boolean
description: Whether to hide the scan from the public catalog
SkillScanInput:
type: object
required: [source, useAi]
properties:
name:
type: ["string", "null"]
description: Optional display name for the scan
source:
type: string
enum: [zip, file, text]
useAi:
type: boolean
description: Whether to also run the configured AI analysis
language:
type: ["string", "null"]
enum: [de, en, es, null]
description: Language for the report content (AI output and static findings). Defaults to "de".
contentBase64:
type: ["string", "null"]
description: Base64 content for source=zip or source=file
filename:
type: ["string", "null"]
description: Original filename for source=file or source=zip
text:
type: ["string", "null"]
description: Raw skill text for source=text
Scan:
type: object
required:
- id
- name
- language
- source
- status
- verdict
- riskScore
- fileCount
- aiUsed
- findingCounts
- fingerprint
- relation
- similarity
- comparedScanId
- hidden
- createdAt
properties:
id:
type: integer
name:
type: string
description:
type: ["string", "null"]
description: AI-generated summary of the skill's purpose (null when no AI description is available)
language:
type: string
enum: [de, en, es]
description: Language the report content was generated in
source:
type: string
enum: [zip, file, text]
status:
type: string
enum: [completed, failed]
verdict:
type: string
enum: [pass, review, block]
riskScore:
type: integer
fileCount:
type: integer
aiUsed:
type: boolean
aiError:
type: ["string", "null"]
findingCounts:
$ref: "#/components/schemas/FindingCounts"
fingerprint:
type: string
description: Deterministic hash over all files (path + per-file hash)
relation:
type: ["string", "null"]
enum: [new, identical, modified, null]
description: Relation to previously stored skills
similarity:
type: ["integer", "null"]
description: Content-aware similarity (0-100) to the compared skill (identical files count fully, changed text files use line-level similarity)
comparedScanId:
type: ["integer", "null"]
description: The scan this one was compared against, if any
hidden:
type: boolean
description: Whether an admin has hidden this scan from the public catalog
createdAt:
type: string
FindingCounts:
type: object
required: [critical, high, medium, low, info, security, privacy, total]
properties:
critical:
type: integer
high:
type: integer
medium:
type: integer
low:
type: integer
info:
type: integer
security:
type: integer
privacy:
type: integer
total:
type: integer
ScanCheckpoint:
type: object
description: >-
A single inspection step (Prüfschritt) with its partial assessment
(Teilbewertung).
required: [id, label, category, status, findingCount, scoreDelta]
properties:
id:
type: string
label:
type: string
category:
type: string
axis:
type: ["string", "null"]
enum: [security, privacy, null]
severity:
type: ["string", "null"]
enum: [critical, high, medium, low, info, null]
status:
type: string
enum: [pass, flagged, skipped, error]
findingCount:
type: integer
scoreDelta:
type: integer
detectedBy:
type: ["string", "null"]
enum: [static, ai, null]
ScanFile:
type: object
required: [path, kind, size, hash, hasContent]
properties:
path:
type: string
kind:
type: string
enum: [instruction, script, resource]
language:
type: ["string", "null"]
size:
type: integer
hash:
type: string
description: SHA-256 hash of the file content
hasContent:
type: boolean
description: Whether the text content was stored (false for binary files)
content:
type: ["string", "null"]
description: The stored text content of the file, or null for binary files
Finding:
type: object
required:
- id
- ruleId
- axis
- severity
- title
- description
- detectedBy
properties:
id:
type: integer
ruleId:
type: string
axis:
type: string
enum: [security, privacy]
severity:
type: string
enum: [critical, high, medium, low, info]
title:
type: string
description:
type: string
remediation:
type: ["string", "null"]
file:
type: ["string", "null"]
line:
type: ["integer", "null"]
snippet:
type: ["string", "null"]
detectedBy:
type: string
enum: [static, ai]
ScanDetail:
allOf:
- $ref: "#/components/schemas/Scan"
- type: object
required: [files, findings, checkpoints, checkCount, comparedScan]
properties:
files:
type: array
items:
$ref: "#/components/schemas/ScanFile"
findings:
type: array
items:
$ref: "#/components/schemas/Finding"
checkpoints:
type: array
items:
$ref: "#/components/schemas/ScanCheckpoint"
checkCount:
type: integer
description: How often a skill with this exact fingerprint was scanned
comparedScan:
oneOf:
- $ref: "#/components/schemas/ComparedScan"
- type: "null"
ComparedScan:
type: object
required: [id, name, verdict, riskScore, createdAt]
properties:
id:
type: integer
name:
type: string
verdict:
type: string
enum: [pass, review, block]
riskScore:
type: integer
createdAt:
type: string
ScanLineageEntry:
type: object
required:
- id
- name
- verdict
- riskScore
- relation
- similarity
- comparedScanId
- fingerprint
- createdAt
properties:
id:
type: integer
name:
type: string
verdict:
type: string
enum: [pass, review, block]
riskScore:
type: integer
relation:
type: ["string", "null"]
enum: [new, identical, modified, null]
description: Relation of this version to the one it was compared against
similarity:
type: ["integer", "null"]
description: Content-aware similarity (0-100) to its compared version
comparedScanId:
type: ["integer", "null"]
description: The prior version this scan was compared against, if any
fingerprint:
type: string
createdAt:
type: string
ScanComparisonSide:
type: object
required: [id, name, verdict, riskScore, fileCount, fingerprint, createdAt]
properties:
id:
type: integer
name:
type: string
verdict:
type: string
enum: [pass, review, block]
riskScore:
type: integer
fileCount:
type: integer
fingerprint:
type: string
createdAt:
type: string
DiffLine:
type: object
required: [type, text, previousLine, currentLine]
properties:
type:
type: string
enum: [context, add, remove]
text:
type: string
previousLine:
type: ["integer", "null"]
currentLine:
type: ["integer", "null"]
ScanFileDiff:
type: object
required:
- path
- status
- previousHash
- currentHash
- previousSize
- currentSize
- previousHasContent
- currentHasContent
- lineDiff
properties:
path:
type: string
status:
type: string
enum: [unchanged, modified, added, removed]
previousHash:
type: ["string", "null"]
currentHash:
type: ["string", "null"]
previousSize:
type: ["integer", "null"]
currentSize:
type: ["integer", "null"]
previousHasContent:
type: ["boolean", "null"]
currentHasContent:
type: ["boolean", "null"]
lineDiff:
oneOf:
- type: array
items:
$ref: "#/components/schemas/DiffLine"
- type: "null"
ScanComparison:
type: object
required: [current, previous, files]
properties:
current:
$ref: "#/components/schemas/ScanComparisonSide"
previous:
$ref: "#/components/schemas/ScanComparisonSide"
files:
type: array
items:
$ref: "#/components/schemas/ScanFileDiff"
AiProvider:
type: object
required:
- id
- name
- apiType
- baseUrl
- model
- enabled
- hasToken
- tokenPreview
- createdAt
properties:
id:
type: integer
name:
type: string
apiType:
type: string
enum: [openai, anthropic, custom]
baseUrl:
type: string
model:
type: string
enabled:
type: boolean
hasToken:
type: boolean
tokenPreview:
type: string
description: Masked preview of the stored token (e.g. "sk-...abcd")
createdAt:
type: string
AiProviderInput:
type: object
required: [name, apiType, baseUrl, model]
properties:
name:
type: string
minLength: 1
apiType:
type: string
enum: [openai, anthropic, custom]
baseUrl:
type: string
minLength: 1
model:
type: string
minLength: 1
apiToken:
type: string
enabled:
type: boolean
AiProviderUpdate:
type: object
properties:
name:
type: string
minLength: 1
apiType:
type: string
enum: [openai, anthropic, custom]
baseUrl:
type: string
minLength: 1
model:
type: string
minLength: 1
apiToken:
type: string
description: Provide to replace the stored token; omit to keep existing
enabled:
type: boolean
ProviderTestResult:
type: object
required: [ok]
properties:
ok:
type: boolean
message:
type: ["string", "null"]
ProviderTestConnectionInput:
type: object
required: [apiType, baseUrl]
properties:
apiType:
type: string
enum: [openai, anthropic, custom]
baseUrl:
type: string
minLength: 1
model:
type: string
description: Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead
apiToken:
type: string
description: Token to use for the test; omit or leave empty to fall back to the stored token of providerId
providerId:
type: integer
description: When apiToken is empty, fall back to this saved provider's stored token
ProviderListModelsInput:
type: object
required: [apiType, baseUrl]
properties:
apiType:
type: string
enum: [openai, anthropic, custom]
baseUrl:
type: string
minLength: 1
apiToken:
type: string
description: Token to use for the request; omit or leave empty to fall back to the stored token of providerId
providerId:
type: integer
description: When apiToken is empty, fall back to this saved provider's stored token
ProviderModelsResult:
type: object
required: [ok, models]
properties:
ok:
type: boolean
models:
type: array
items:
type: string
message:
type: ["string", "null"]
Prompt:
type: object
required: [id, key, name, content, updatedAt]
properties:
id:
type: integer
key:
type: string
name:
type: string
content:
type: string
updatedAt:
type: string
PromptUpdate:
type: object
properties:
name:
type: string
minLength: 1
content:
type: string
minLength: 1
Rule:
type: object
required:
- id
- ruleId
- axis
- category
- title
- description
- severity
- detectionType
- enabled
properties:
id:
type: integer
ruleId:
type: string
axis:
type: string
enum: [security, privacy]
category:
type: string
title:
type: string
description:
type: string
severity:
type: string
enum: [critical, high, medium, low, info]
detectionType:
type: string
enum: [regex, heuristic, ai]
enabled:
type: boolean
RuleUpdate:
type: object
properties:
severity:
type: string
enum: [critical, high, medium, low, info]
enabled:
type: boolean
DashboardSummary:
type: object
required:
- totalScans
- avgRiskScore
- verdictCounts
- severityTotals
- axisTotals
- recentScans
- topRules
properties:
totalScans:
type: integer
avgRiskScore:
type: integer
verdictCounts:
$ref: "#/components/schemas/VerdictCounts"
severityTotals:
$ref: "#/components/schemas/SeverityTotals"
axisTotals:
$ref: "#/components/schemas/AxisTotals"
recentScans:
type: array
items:
$ref: "#/components/schemas/Scan"
topRules:
type: array
items:
$ref: "#/components/schemas/RuleStat"
VerdictCounts:
type: object
required: [pass, review, block]
properties:
pass:
type: integer
review:
type: integer
block:
type: integer
SeverityTotals:
type: object
required: [critical, high, medium, low, info]
properties:
critical:
type: integer
high:
type: integer
medium:
type: integer
low:
type: integer
info:
type: integer
AxisTotals:
type: object
required: [security, privacy]
properties:
security:
type: integer
privacy:
type: integer
RuleStat:
type: object
required: [ruleId, title, axis, count]
properties:
ruleId:
type: string
title:
type: string
axis:
type: string
enum: [security, privacy]
count:
type: integer