API & Authentifizierung
Oberflächen-, API- und Umgebungsgrenze
Die Oberfläche meldet Benutzer über die Felder „Badge“, „PIN“ und optional „Station“ sowie die Schaltfläche „Anmelden“ an. Sie verwendet danach ein HttpOnly-Sitzungscookie. Ein eigenständiger HTTP-Client kann dasselbe Sitzungstoken als Bearer-Token senden.
Die laufende Authentifizierung wird nicht über Umgebungsvariablen konfiguriert. Nur der erste Admin einer leeren Installation stammt aus FOUNDRY_BOOTSTRAP_ADMIN_BADGE und FOUNDRY_BOOTSTRAP_ADMIN_PIN. Weitere Benutzer legt ein Admin mit einer Rolle über die Anwendung an. Einen Endpunkt oder UI-Ablauf zum nachträglichen Ändern einer bestehenden Benutzerrolle gibt es derzeit nicht.
Für dauerhafte Zugangsdaten unbeaufsichtigter Systeme gibt es API-Tokens (siehe unten). Eine HTTP-Basic-Anmeldung und einen OIDC- oder LDAP-Anmelde-Endpunkt gibt es weiterhin nicht. Ein Webhook nutzt zwar ebenfalls den Header Authorization: Bearer ..., aber sein Token ist der Wert eines Trigger-Secrets, keine Benutzersitzung und kein API-Token.
Alle Beispiele verwenden Platzhalter:
export WERKSPUR='https://werkspur.internal.invalid'
Sitzung erstellen
POST /api/sessions ist ohne bestehende Sitzung erreichbar. Senden Sie Badge, PIN und eine optionale Station als JSON:
curl -sS -X POST "$WERKSPUR/api/sessions" \
-H 'Content-Type: application/json' \
-d '{
"badge": "<BADGE>",
"pin": "<PIN>",
"terminal": "<OPTIONAL_STATION>"
}'
terminal darf für jede Rolle leer bleiben. Ist es nicht leer, muss der Stationsname für operator und planner im Stationsregister vorhanden sein. Ein unbekannter nicht leerer Name wird erst nach erfolgreicher Prüfung von Badge und PIN mit 422 und dem Fehlercode auth.unknown_station abgelehnt; dabei entsteht keine Sitzung. admin umgeht diese Stationsprüfung, und der eingegebene Name wird dennoch in der Sitzung gespeichert. Verwaltung und Bedeutung der Stationen beschreibt Rollen & Stationen.
Nach fünf falschen PIN-Eingaben in Folge sperrt der Server den Benutzer für fünf Minuten. Ein Login-Versuch während dieser Sperre antwortet mit 423 und {"error":"too many failed attempts, try again later"}.
Bei Erfolg antwortet der Server mit 201, setzt das Cookie foundry_session und liefert dieselbe Sitzung für API-Clients:
{
"token": "<SESSION_TOKEN>",
"expires_at": "<RFC3339_TIMESTAMP>",
"user": {
"id": "<USER_ID>",
"badge": "<BADGE>",
"name": "<DISPLAY_NAME>",
"role": "admin"
}
}
Eine Sitzung gilt zwölf Stunden. Der Server speichert nur den SHA-256-Hash des zufälligen Tokens. Das Cookie ist HttpOnly und SameSite=Lax. Es erhält das Attribut Secure, wenn die Anfrage direkt über TLS oder über einen Proxy mit X-Forwarded-Proto: https eingetroffen ist.
Für Skripte übernehmen Sie den zurückgegebenen Wert nicht in Quellcode oder Versionsverwaltung, sondern halten ihn nur für die Laufzeit des Prozesses:
export WERKSPUR_SESSION='<SESSION_TOKEN>'
curl -sS \
-H "Authorization: Bearer $WERKSPUR_SESSION" \
"$WERKSPUR/api/sessions/current"
DELETE /api/sessions/current widerruft die verwendete Sitzung und antwortet mit 204. Das Deaktivieren eines Benutzers macht seine bestehenden Sitzungen bei der nächsten Prüfung ungültig. Abgelaufene, widerrufene oder unbekannte Tokens erhalten 401.
Rollenprüfung
Die Rollen sind hierarchisch:
operatordarf Endpunkte ab der Bedienerrolle verwenden.plannerschließt die Rechte vonoperatorein.adminschließt beide niedrigeren Rollen ein.
Jeder geschützte Endpunkt nennt im Servercode eine Mindestrolle. Die Authentifizierung und die fachliche Aktionsrolle sind zwei Prüfungen: Die HTTP-Middleware prüft zuerst die Sitzung. Beim Aufruf einer Aktion prüft der Executor danach zusätzlich deren min_role.
Ohne Token oder Cookie antwortet ein geschützter Endpunkt mit 401 und {"error":"authentication required"}. Ein ungültiges oder abgelaufenes Token ergibt 401 mit {"error":"invalid or expired session"}. Eine gültige Sitzung mit zu niedriger Rolle ergibt 403 mit {"error":"insufficient role"}.
Teilen Sie Sitzungstokens nicht zwischen Personen oder Stationen. Der bei der Anmeldung angegebene Stationsname wird mit der Sitzung gespeichert und in Audit-Metadaten nachfolgender Kommandos übernommen.
API-Tokens
Für unbeaufsichtigte Systeme, etwa ein anbindendes ERP, sind Sitzungen ungeeignet: Niemand meldet sich für einen Cron- oder Importlauf mit Badge und PIN an. API-Tokens sind die dauerhafte Alternative dazu. Ein Admin legt ein Token unter Administration → API-Tokens an und vergibt dabei einen Namen und eine Rolle. Der Auftragsimport (POST /api/orders, POST /api/orders/import) braucht mindestens die Rolle planner; eine niedrigere Rolle scheitert an der normalen Rollenprüfung aus dem vorherigen Abschnitt.
Der Server zeigt den Tokenwert genau einmal, direkt nach dem Anlegen. Gespeichert wird nur sein SHA-256-Hash, derselbe Ansatz wie bei Sitzungen. Notieren Sie den Wert sofort an einem sicheren Ort; ein späterer Abruf über die API oder die Oberfläche ist danach nicht mehr möglich.
Ein Aufrufer sendet das Token mit dem Präfix wsk_ als Bearer-Token, genau wie ein Sitzungstoken:
curl -sS -X POST "$WERKSPUR/api/orders" \
-H 'Authorization: Bearer wsk_<TOKEN_VALUE>' \
-H 'Content-Type: application/json' \
-d '{"number":"BEISPIEL-1004","item":"Bauteil-A","quantity":50,"due_date":"2026-08-06T12:00:00Z"}'
Ein Admin widerruft ein Token unter Administration → API-Tokens. Der Widerruf wirkt sofort: Die nächste Anfrage mit diesem Token erhält 401. Anders als eine Sitzung läuft ein Token nicht von selbst ab; es bleibt gültig, bis es widerrufen wird. Der Name eines widerrufenen Tokens bleibt vergeben. Legen Sie für einen Ersatz einen neuen Namen an.
Idempotente Aktionsaufrufe
Idempotenz ist kein globales API-Middleware-Verhalten. Sie gilt gezielt für POST /api/actions/{name}/invoke, wenn der Client einen Header Idempotency-Key sendet. Ohne Header läuft der Aufruf ohne Deduplizierung.
curl -sS -X POST "$WERKSPUR/api/actions/<ACTION_NAME>/invoke" \
-H "Authorization: Bearer $WERKSPUR_SESSION" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 47aa2f20-8f0d-4dc6-bca3-a4d825f92124' \
-d '{"inputs": {"reference": "<REFERENCE>"}}'
Der Schlüssel muss 8 bis 128 Zeichen lang sein und darf nur Buchstaben, Ziffern und Bindestriche enthalten. Eine Wiederholung mit demselben Schlüssel und demselben Aktionsnamen führt die Aktion nicht erneut aus. Sie liefert die gespeicherte Antwort mit 200 und dem Antwort-Header Idempotency-Replayed: true.
Der Schlüssel und die erfolgreiche Antwort werden in derselben Transaktion wie die Aktion gespeichert. Scheitert die Aktion, bleibt kein Deduplizierungseintrag zurück, und eine fachlich zulässige Wiederholung kann die Aktion erneut aufrufen. Ein gleichzeitig eintreffender identischer Aufruf wartet auf das erste Festschreiben und erhält danach die gespeicherte Antwort.
Erzeugen Sie für jeden beabsichtigten neuen Lauf einen neuen Schlüssel. Verwenden Sie denselben Schlüssel nie für verschiedene Aktionsnamen. Das System gibt in diesem Fall nicht versehentlich die Antwort der anderen Aktion zurück, sondern lehnt den zweiten Aufruf mit 409 und dem Fehlercode action.idempotency_key_conflict ab, da der Schlüssel allein als Primärschlüssel gilt. Ein stündlicher Bereinigungslauf entfernt Deduplizierungseinträge, die älter als 72 Stunden sind. Ein Schlüssel ist deshalb kein dauerhafter Geschäftsbeleg.
Webhook-Bearer
POST /api/hooks/{name} verwendet keine Benutzersitzung und keine Rollen-Middleware. Der Webhook-Trigger referenziert mit webhook.secret den Namen eines gespeicherten Secrets. Der Aufrufer sendet dessen Wert mit exakt diesem Schema:
curl -sS -X POST "$WERKSPUR/api/hooks/<WEBHOOK_NAME>" \
-H 'Authorization: Bearer <WEBHOOK_SECRET_VALUE>' \
-H 'Content-Type: application/json' \
-d '{"reference":"<REFERENCE>"}'
Der Server löst das Secret bei jedem Aufruf auf und vergleicht den Wert in konstanter Zeit. Ein fehlendes Secret, ein fehlender Bearer-Präfix oder ein falscher Wert ergibt 401. Ein unbekannter, deaktivierter oder nicht als Webhook definierter Trigger ergibt 404.
Der Body des HTTP-Requests darf leer sein. Der Server liest höchstens die ersten 1 MiB. Einen nicht leeren gelesenen Body dekodiert er als JSON-Objekt; Arrays, Strings, Zahlen und boolesche Werte ergeben 400. Der JSON-Wert null verhält sich wie ein leeres Objekt.
Bei einem größeren Body ist der gelesene Präfix durch die Begrenzung in der Regel kein vollständiges JSON und ergibt dann ebenfalls 400. Es gibt dafür keinen eigenen 413-Pfad. Verlassen Sie sich oberhalb von 1 MiB nicht auf ein bestimmtes Ergebnis, sondern halten Sie den vollständigen Body innerhalb der Grenze.
Werte für die Aktion stammen aus den im Trigger definierten Platzhaltern ${body.<pfad>}. Ein fehlender oder fehlerhafter Platzhalter ergibt 422. Schlägt die ausgelöste Aktion fehl, antwortet der Webhook mit 502. Bei Erfolg liefert er den aufgezeichneten Triggerlauf mit 200.
Speichern Sie ein Webhook-Secret nicht als Sitzungstoken und umgekehrt. Eine Admin-Sitzung autorisiert keinen Webhook-Aufruf; ein Webhook-Secret öffnet keinen Admin-Endpunkt.
Statuscodes und Body der HTTP-Response
WERKSPUR liefert JSON. Nicht jeder Endpunkt verwendet jeden Status, aber die Formen sind konsistent:
| Status | Typische Bedeutung | Body der HTTP-Response |
|---|---|---|
| 200, 201 | Anfrage erfolgreich | Endpunktspezifisches JSON |
| 204 | Erfolgreich, kein Body | leer |
| 400 | JSON oder Anfrageparameter syntaktisch ungültig | {"error":"..."} |
| 401 | Sitzung, API-Token oder Webhook-Bearer fehlt oder ist ungültig | {"error":"..."} |
| 403 | Rolle reicht nicht aus | {"error":"..."} |
| 404 | Ressource oder aktivierter Webhook fehlt | {"error":"..."} |
| 409 | Konflikt, beispielsweise eine nicht erfüllte Aktionsbedingung | einfacher oder codierter Fehler |
| 422 | Struktur oder fachliche Eingabe ungültig | einfacher oder codierter Fehler |
| 423 | Login nach fünf Fehlversuchen für fünf Minuten gesperrt | {"error":"..."} |
| 502 | Externer Aufruf oder per Webhook ausgelöste Aktion fehlgeschlagen | {"error":"..."} oder Triggerlauf |
| 503 | Abhängigkeit oder optionale Funktion nicht verfügbar | einfacher oder codierter Fehler |
Die Minimalform lautet:
{"error":"human-readable fallback"}
Ein stabil codierter fachlicher Fehler ergänzt code und gegebenenfalls params:
{
"error": "human-readable fallback",
"code": "domain.error_code",
"params": {"field": "<VALUE>"}
}
Bei erkannter PostgreSQL-Nichtverfügbarkeit lautet die kanonische Antwort:
{"error":"backend unavailable","code":"backend_unavailable"}
Automatisierung soll zuerst den HTTP-Status und danach einen vorhandenen stabilen code auswerten. Der englische Text in error ist ein Fallback und nicht für eine feste Textanalyse vorgesehen. Viele Endpunkte liefern noch keinen code.
Liveness und Readiness
GET /healthz und GET /readyz brauchen keine Anmeldung.
/healthz prüft nur, ob der HTTP-Prozess antwortet. Es liefert bei einem laufenden Prozess 200 und {"status":"ok"}. Daraus folgt nicht, dass Datenbank, NATS oder ein externes System erreichbar sind.
/readyz prüft jede konfigurierte Kernabhängigkeit mit einem Zeitlimit von zwei Sekunden. Ist PostgreSQL konfiguriert, erscheint postgres. Ist NATS konfiguriert, erscheint nats. Ein Fehler setzt den Gesamtstatus auf 503 und status auf unavailable; die jeweilige Meldung steht in checks.
{
"status": "unavailable",
"checks": {
"postgres": "ok",
"nats": "<ERROR>"
}
}
Nicht konfigurierte Kernabhängigkeiten werden nicht geprüft. Ohne Checks kann /readyz deshalb ok melden. Der Endpunkt prüft außerdem weder REST-, SOAP-, MQTT-, AMQP- oder OPC-UA-Ziele noch Drucker oder KI-Anbieter. Verwenden Sie für diese Systeme die protokollspezifische Folge unter Integration: Fehlersuche.
Sichere Betriebsregeln
- Verwenden Sie TLS, sobald Sitzungs- oder Webhook-Tokens ein nicht vollständig abgeschottetes Netz durchlaufen.
- Protokollieren Sie keine
Authorization-Header und keine Session- oder Secret-Werte. - Setzen Sie
Content-Type: application/jsonbei JSON-Bodies und behandeln Sie 204 ohne JSON-Parsing. - Wiederholen Sie mutierende Aktionsaufrufe nur mit demselben Idempotenzschlüssel. Für andere mutierende Endpunkte gibt es diese Deduplizierungsgarantie nicht.
- Nutzen Sie für die Diagnose Platzhalter und Metadaten. Echte Tokens, PINs und Secret-Werte gehören weder in Tickets noch in gemeinsam genutzte Kommandohistorien.