{
  "openapi": "3.0.3",
  "info": {
    "title": "SecretNotes API",
    "version": "2.0.0",
    "description": "Eine Notiz pro Ableton-Set, pro Nutzer, teilbar mit anderen Nutzern.\n\nAlles JSON (UTF-8). Fehler immer als `{\"error\":\"<code>\"}` mit passendem HTTP-Status.\n\nAuthentifizierung: `Authorization: Bearer smn_<hex>` (API-Key, für Geräte/Skripte/n8n) oder Session-Cookie `smn_session` (nur Website, wird von `POST /api/login` ohne `device` gesetzt).\n\nNotiznamen im Pfad müssen URL-kodiert sein (Leerzeichen → `%20`).",
    "contact": {
      "url": "https://notes.secret.vip/api/docs"
    }
  },
  "servers": [
    {
      "url": "https://notes.secret.vip"
    }
  ],
  "security": [
    { "bearerAuth": [] },
    { "cookieAuth": [] }
  ],
  "tags": [
    { "name": "Konten", "description": "Registrierung, Login, eigenes Konto" },
    { "name": "Notizen", "description": "Eine Notiz pro Ableton-Set und Nutzer" },
    { "name": "Teilen", "description": "Freigaben einer Notiz – nur Besitzer" },
    { "name": "API-Keys", "description": "Eigene Bearer-Tokens" },
    { "name": "Admin", "description": "Nur für Nutzer mit is_admin" },
    { "name": "Sonstiges", "description": "Health, Doku, OpenAPI" }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": ["Sonstiges"],
        "summary": "Lebenszeichen des Servers",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "Server läuft",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Ok" },
                "example": { "ok": true }
              }
            }
          }
        }
      }
    },
    "/api/docs": {
      "get": {
        "tags": ["Sonstiges"],
        "summary": "HTML-Dokumentation",
        "operationId": "getDocs",
        "security": [],
        "responses": {
          "200": {
            "description": "Dokumentation als HTML",
            "content": {
              "text/html": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/api/openapi.json": {
      "get": {
        "tags": ["Sonstiges"],
        "summary": "OpenAPI-Beschreibung dieser API",
        "operationId": "getOpenapi",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.0 Dokument",
            "content": {
              "application/json": {
                "schema": { "type": "object" }
              }
            }
          }
        }
      }
    },
    "/api/register": {
      "post": {
        "tags": ["Konten"],
        "summary": "Konto anlegen",
        "description": "Legt ein Konto an. Auf notes.secret.vip gilt Double-Opt-in (`email_verification` in `/api/register/info`): Es wird **kein** Cookie gesetzt, sondern eine Bestätigungs-Mail geschickt (Link `/?verify=<token>`, 24 Stunden gültig); die Antwort enthält `verification_required:true` und `verification_sent`, anmelden geht erst nach `POST /api/register/verify`. Erstes Konto, Einladungen und Server ohne E-Mail-Versand sind sofort angemeldet (Session-Cookie). Nicht bestätigte Konten werden nach 7 Tagen gelöscht. Erlaubt, wenn noch kein Nutzer existiert (der erste wird Admin) **oder** `invite` gültig ist **oder** der Server offen ist (`SMN_OPEN_REGISTRATION=1`). Captcha (`GET /api/captcha`) ist Pflicht, außer beim ersten Konto oder mit gültigem `invite`. Das Feld `website` ist ein Honeypot und muss leer bleiben. Limit: 5 Registrierungen pro IP und Stunde.",
        "operationId": "register",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "name", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email", "description": "Wird kleingeschrieben gespeichert" },
                  "name": { "type": "string", "description": "Anzeigename" },
                  "password": { "type": "string", "format": "password", "minLength": 8 },
                  "invite": { "type": "string", "description": "Einladungs-Token aus dem Einladungslink (optional)" },
                  "captcha": { "$ref": "#/components/schemas/CaptchaAnswer" },
                  "website": { "type": "string", "description": "Honeypot – weglassen oder leer lassen; jeder Inhalt ergibt 400 invalid_body" }
                }
              },
              "example": { "email": "neu@example.com", "name": "Neu", "password": "korrekt-pferd-batterie", "captcha": { "id": "0f3a…", "answer": "k7mpx" } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Konto angelegt – entweder angemeldet (Set-Cookie, `{user}`) oder Bestätigung ausstehend (`{user, verification_required:true, verification_sent}`, kein Cookie)",
            "headers": {
              "Set-Cookie": {
                "description": "Session-Cookie `smn_session` (HttpOnly, 30 Tage)",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/UserEnvelope" },
                    { "type": "object", "properties": {
                      "verification_required": { "type": "boolean", "description": "Nur bei Double-Opt-in: Konto ist bis zur Bestätigung gesperrt, kein Cookie gesetzt" },
                      "verification_sent": { "type": "boolean", "description": "Bestätigungs-Mail wurde versendet; `false` → über POST /api/register/resend neu anfordern" } } }
                  ]
                },
                "examples": {
                  "bestaetigung_ausstehend": { "value": { "user": { "id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": false, "created_at": "2026-09-03T09:00:00.000Z" }, "verification_required": true, "verification_sent": true } },
                  "sofort_angemeldet": { "value": { "user": { "id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": true, "created_at": "2026-09-03T09:00:00.000Z" } } }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body`, `weak_password` (min. 8 Zeichen, kein Allerweltspasswort, nicht die E-Mail), `invalid_invite`, `registration_closed`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_body": { "value": { "error": "invalid_body" } },
                  "weak_password": { "value": { "error": "weak_password" } },
                  "invalid_invite": { "value": { "error": "invalid_invite" } },
                  "registration_closed": { "value": { "error": "registration_closed" } }
                }
              }
            }
          },
          "403": { "$ref": "#/components/responses/CaptchaRequired" },
          "409": {
            "description": "`email_taken` – E-Mail bereits registriert",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "email_taken" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/register/info": {
      "get": {
        "tags": ["Konten"],
        "summary": "Ist Registrierung möglich? Optional Einladung prüfen",
        "operationId": "getRegisterInfo",
        "security": [],
        "parameters": [
          {
            "name": "invite",
            "in": "query",
            "required": false,
            "description": "Einladungs-Token. Wenn gesetzt, enthält die Antwort zusätzlich `invite_valid` und `invite_email`.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Registrierungsstatus",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RegisterInfo" },
                "examples": {
                  "ohne_invite": { "value": { "open": true, "first_user": false, "captcha_required": true, "password_reset": true, "email_verification": true, "min_password": 8 } },
                  "mit_invite": { "value": { "open": true, "first_user": false, "captcha_required": true, "password_reset": true, "email_verification": true, "min_password": 8, "invite_valid": true, "invite_email": "neu@example.com" } }
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/register/verify": {
      "post": {
        "tags": ["Konten"],
        "summary": "E-Mail-Adresse mit dem Token aus der Bestätigungs-Mail bestätigen",
        "description": "Löst den Token aus dem Link `https://notes.secret.vip/?verify=<token>` ein (24 Stunden gültig, einmal nutzbar), schaltet das Konto frei und setzt ein Session-Cookie. Ungültige Tokens zählen als Fehlversuch (Login-Bremse).",
        "operationId": "verifyEmail",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["token"], "properties": { "token": { "type": "string", "description": "Token aus dem Bestätigungslink" } } },
              "example": { "token": "ver_…" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bestätigt und angemeldet (Set-Cookie)",
            "headers": { "Set-Cookie": { "description": "Session-Cookie `smn_session` (HttpOnly, 30 Tage)", "schema": { "type": "string" } } },
            "content": {
              "application/json": {
                "schema": { "type": "object", "required": ["ok", "user"], "properties": { "ok": { "type": "boolean" }, "user": { "$ref": "#/components/schemas/User" } } },
                "example": { "ok": true, "user": { "id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": true, "created_at": "2026-09-03T09:00:00.000Z" } }
              }
            }
          },
          "400": {
            "description": "`invalid_body`, `invalid_token` (ungültig, abgelaufen oder schon benutzt)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "invalid_token" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/register/resend": {
      "post": {
        "tags": ["Konten"],
        "summary": "Bestätigungs-Mail erneut senden",
        "description": "Schickt die Bestätigungs-Mail (Absender hello@secret.vip) erneut, falls zu der Adresse ein noch nicht bestätigtes Konto existiert. Antwortet **immer** `{ok:true}`. Captcha ist Pflicht. Limits: 3 Mails pro E-Mail und Stunde, 10 Anfragen pro IP und Stunde.",
        "operationId": "resendVerification",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["email", "captcha"], "properties": { "email": { "type": "string", "format": "email" }, "captcha": { "$ref": "#/components/schemas/CaptchaAnswer" } } },
              "example": { "email": "neu@example.com", "captcha": { "id": "0f3a…", "answer": "k7mpx" } }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "403": { "$ref": "#/components/responses/CaptchaRequired" },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "503": {
            "description": "`mail_unavailable` – E-Mail-Versand auf diesem Server nicht eingerichtet",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "mail_unavailable" } } }
          }
        }
      }
    },
    "/api/login": {
      "post": {
        "tags": ["Konten"],
        "summary": "Anmelden (Session-Cookie oder Geräte-Token)",
        "description": "Ohne `device`: Session-Cookie wird gesetzt, Antwort `{user}`. Mit `device` (z. B. `\"Ableton Live (MacBook)\"`): legt einen API-Key mit diesem Namen an und gibt `{token, user}` zurück – kein Cookie.",
        "operationId": "login",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "format": "password" },
                  "device": { "type": "string", "description": "Gerätename. Wenn gesetzt: API-Key wird angelegt, Token statt Cookie." },
                  "captcha": { "$ref": "#/components/schemas/CaptchaAnswer", "description": "Nur nötig nach 403 captcha_required (3 Fehlversuche pro IP oder E-Mail)" }
                }
              },
              "examples": {
                "website": { "value": { "email": "ich@example.com", "password": "geheim123" } },
                "geraet": { "value": { "email": "ich@example.com", "password": "geheim123", "device": "Ableton Live (MacBook)" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Angemeldet. Ohne `device` mit Set-Cookie und `{user}`; mit `device` `{token, user}`.",
            "headers": {
              "Set-Cookie": {
                "description": "Nur ohne `device`: Session-Cookie `smn_session` (HttpOnly, 30 Tage)",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LoginResult" },
                "examples": {
                  "website": {
                    "value": { "user": { "id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z" } }
                  },
                  "geraet": {
                    "value": { "token": "smn_…", "user": { "id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "401": {
            "description": "`wrong_credentials` – E-Mail oder Passwort falsch",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "wrong_credentials" }
              }
            }
          },
          "403": {
            "description": "`captcha_required` – Captcha nötig, fehlt oder falsch (neues Bild über GET /api/captcha); `email_unverified` – E-Mail-Adresse noch nicht bestätigt (Link aus der Bestätigungs-Mail öffnen oder POST /api/register/resend); mit `device`: `quota_exceeded` – schon 50 Geräte-Zugänge",
            "content": {
              "application/json": {
                "schema": { "oneOf": [ { "$ref": "#/components/schemas/Error" }, { "$ref": "#/components/schemas/QuotaError" } ] },
                "examples": {
                  "captcha_required": { "value": { "error": "captcha_required" } },
                  "quota_exceeded": { "value": { "error": "quota_exceeded", "limit": 50, "what": "keys" } }
                }
              }
            }
          },
          "429": {
            "description": "`too_many_attempts` – 10 Fehlversuche in 15 Minuten pro IP",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "too_many_attempts" }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/captcha": {
      "get": {
        "tags": ["Konten"],
        "summary": "Sicherheitscode (Captcha) als SVG holen",
        "description": "Selbst gehostetes Bild-Captcha, kein Drittanbieter. Der Code ist einmal verwendbar und 10 Minuten gültig; jede Prüfung verbraucht ihn (nach falscher Antwort neu holen). Groß-/Kleinschreibung egal. Nötig bei Registrierung, Passwort vergessen und beim Login nach `403 captcha_required` (3 Fehlversuche pro IP oder E-Mail). Limit: 30 Bilder pro IP und 10 Minuten.",
        "operationId": "getCaptcha",
        "security": [],
        "responses": {
          "200": {
            "description": "Captcha",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Captcha" },
                "example": { "id": "0f3a…", "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"180\" height=\"60\">…</svg>", "ttl": 600 }
              }
            }
          },
          "404": {
            "description": "`not_found` – Captcha auf diesem Server abgeschaltet",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "not_found" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/password/forgot": {
      "post": {
        "tags": ["Konten"],
        "summary": "Passwort vergessen – Link per E-Mail anfordern",
        "description": "Schickt einen einmal nutzbaren Link (`https://notes.secret.vip/?reset=<token>`, 1 Stunde gültig) an die Konto-E-Mail, Absender hello@secret.vip. Antwortet **immer** `{ok:true}`, auch bei unbekannter E-Mail. Captcha ist Pflicht. Limits: 3 Anfragen pro E-Mail und Stunde, 10 pro IP und Stunde.",
        "operationId": "forgotPassword",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "captcha"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "captcha": { "$ref": "#/components/schemas/CaptchaAnswer" }
                }
              },
              "example": { "email": "ich@example.com", "captcha": { "id": "0f3a…", "answer": "k7mpx" } }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "403": { "$ref": "#/components/responses/CaptchaRequired" },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "503": {
            "description": "`mail_unavailable` – E-Mail-Versand auf diesem Server nicht eingerichtet",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "mail_unavailable" } } }
          }
        }
      }
    },
    "/api/password/reset": {
      "post": {
        "tags": ["Konten"],
        "summary": "Neues Passwort mit Token aus der E-Mail setzen",
        "description": "Setzt das Passwort und widerruft **alle** Sitzungen und **alle** Geräte-Zugänge (API-Keys) des Kontos. Setzt keine Session – danach normal anmelden. Ungültige Tokens zählen als Fehlversuch (Login-Bremse).",
        "operationId": "resetPassword",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["token", "password"],
                "properties": {
                  "token": { "type": "string", "description": "`rst_…` aus dem Link" },
                  "password": { "type": "string", "format": "password", "minLength": 8 }
                }
              },
              "example": { "token": "rst_…", "password": "korrekt-pferd-batterie" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": {
            "description": "`invalid_body`, `weak_password`, `invalid_token` (unbekannt, abgelaufen oder schon benutzt)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_token": { "value": { "error": "invalid_token" } },
                  "weak_password": { "value": { "error": "weak_password" } }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyAttempts" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/logout": {
      "post": {
        "tags": ["Konten"],
        "summary": "Abmelden",
        "description": "Beendet die Session bzw. widerruft den API-Key, mit dem der Aufruf gemacht wurde.",
        "operationId": "logout",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/me": {
      "get": {
        "tags": ["Konten"],
        "summary": "Eigenes Konto",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "Eigenes Konto",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UserEnvelope" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "patch": {
        "tags": ["Konten"],
        "summary": "Namen und/oder Passwort ändern",
        "description": "Passwortwechsel nur mit korrektem `current_password`.",
        "operationId": "patchMe",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "password": { "type": "string", "format": "password", "minLength": 8 },
                  "current_password": { "type": "string", "format": "password", "description": "Pflicht, wenn `password` gesetzt ist" }
                }
              },
              "example": { "password": "nochGeheimer456", "current_password": "geheim123" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aktualisiertes Konto",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UserEnvelope" }
              }
            }
          },
          "400": {
            "description": "`invalid_body`, `weak_password`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_body": { "value": { "error": "invalid_body" } },
                  "weak_password": { "value": { "error": "weak_password" } }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` – nicht angemeldet; `wrong_credentials` – `current_password` falsch",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "unauthorized": { "value": { "error": "unauthorized" } },
                  "wrong_credentials": { "value": { "error": "wrong_credentials" } }
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/notes": {
      "get": {
        "tags": ["Notizen"],
        "summary": "Alle eigenen und mit mir geteilten Notizen",
        "description": "Neueste zuerst.",
        "operationId": "listNotes",
        "responses": {
          "200": {
            "description": "Notizliste",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["notes"],
                  "properties": {
                    "notes": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/NoteListItem" }
                    }
                  }
                },
                "example": {
                  "notes": [
                    { "name": "Mein Track", "owner": { "email": "ich@example.com", "name": "Sebastian" }, "mine": true, "permission": "owner", "updated_at": "2026-09-03T10:15:30.123Z", "size": 27 },
                    { "name": "Late Night Jam", "owner": { "email": "freund@example.com", "name": "Alex" }, "mine": false, "permission": "write", "updated_at": "2026-09-02T22:41:09.000Z", "size": 412 }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/notes/{name}": {
      "parameters": [
        { "$ref": "#/components/parameters/NoteName" }
      ],
      "get": {
        "tags": ["Notizen"],
        "summary": "Notiz lesen",
        "description": "Existiert die Notiz nicht, kommt `200` mit `exists:false`, `text:\"\"`, `updated_at:null`. Antwort trägt `ETag:\"<ms>\"`; passendes `If-None-Match` → `304`.",
        "operationId": "getNote",
        "parameters": [
          { "$ref": "#/components/parameters/Owner" },
          { "$ref": "#/components/parameters/IfNoneMatch" }
        ],
        "responses": {
          "200": {
            "description": "Notiz (auch wenn nicht vorhanden – dann `exists:false`)",
            "headers": {
              "ETag": {
                "description": "Änderungszeitpunkt in Millisekunden, in Anführungszeichen, z. B. `\"1788430530123\"`",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Note" },
                "examples": {
                  "vorhanden": {
                    "value": { "name": "Mein Track", "text": "Kick lauter\nVocals ab 1:30", "updated_at": "2026-09-03T10:15:30.123Z", "exists": true, "owner": { "email": "ich@example.com", "name": "Sebastian" }, "mine": true, "permission": "owner" }
                  },
                  "nicht_vorhanden": {
                    "value": { "name": "Neues Set", "text": "", "updated_at": null, "exists": false, "owner": { "email": "ich@example.com", "name": "Sebastian" }, "mine": true, "permission": "owner" }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Nicht geändert – `If-None-Match` passt zum aktuellen `ETag`. Kein Body."
          },
          "400": { "$ref": "#/components/responses/InvalidName" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "put": {
        "tags": ["Notizen"],
        "summary": "Notiz komplett schreiben (anlegen oder ersetzen)",
        "description": "Ersetzt den gesamten Text; legt die Notiz an, falls sie fehlt.\n\n**Konfliktschutz:** Ist `base_updated_at` gesetzt (ISO-String oder `null`) und weicht vom Stand auf dem Server ab → `409 conflict` mit dem Serverstand im Body (`text`, `updated_at`). Ist `base_updated_at` gesetzt, die Notiz fehlt aber → `409` mit `text:\"\"`, `updated_at:null`. Ohne `base_updated_at` gewinnt der letzte Schreiber.\n\n`Content-Type` muss `application/json` sein.",
        "operationId": "putNote",
        "parameters": [
          { "$ref": "#/components/parameters/Owner" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["text"],
                "properties": {
                  "text": { "type": "string", "description": "Kompletter neuer Text, max. 1 MB" },
                  "base_updated_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "Das zuletzt gelesene `updated_at` (ISO) oder `null`, wenn die Notiz beim Lesen nicht existierte. Aktiviert den Konfliktcheck."
                  }
                }
              },
              "examples": {
                "einfach": { "value": { "text": "Kick lauter\nVocals ab 1:30" } },
                "mit_konfliktschutz": { "value": { "text": "Kick lauter\nVocals ab 1:30\nHiHat leiser", "base_updated_at": "2026-09-03T10:15:30.123Z" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Gespeichert",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NoteWriteResult" },
                "example": { "name": "Mein Track", "updated_at": "2026-09-03T10:15:30.123Z", "owner": { "email": "ich@example.com", "name": "Sebastian" }, "permission": "owner" }
              }
            }
          },
          "400": {
            "description": "`invalid_name`, `invalid_json` (kaputtes JSON), `invalid_body`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_name": { "value": { "error": "invalid_name" } },
                  "invalid_json": { "value": { "error": "invalid_json" } },
                  "invalid_body": { "value": { "error": "invalid_body" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "`forbidden` – Freigabe ist nur-lesen; `quota_exceeded` – beim Anlegen schon 1000 Notizen",
            "content": {
              "application/json": {
                "schema": { "oneOf": [ { "$ref": "#/components/schemas/Error" }, { "$ref": "#/components/schemas/QuotaError" } ] },
                "examples": {
                  "forbidden": { "value": { "error": "forbidden" } },
                  "quota_exceeded": { "value": { "error": "quota_exceeded", "limit": 1000, "what": "notes" } }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "`conflict` – Notiz wurde zwischenzeitlich geändert. Body enthält den aktuellen Serverstand. Nichts wurde überschrieben.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Conflict" },
                "example": { "error": "conflict", "name": "Mein Track", "text": "Kick lauter\nVocals ab 1:30\nBass ab 0:45 doppeln", "updated_at": "2026-09-03T10:20:00.000Z", "owner": { "email": "ich@example.com", "name": "Sebastian" }, "permission": "owner" }
              }
            }
          },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMediaType" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "patch": {
        "tags": ["Notizen"],
        "summary": "Text als eigene Zeile anhängen",
        "description": "Hängt `append` als eigene Zeile an; legt die Notiz an, falls nötig. Für Automationen (n8n, Skripte) gedacht – kein Konfliktcheck nötig. Antwort enthält den vollständigen neuen `text`.",
        "operationId": "appendNote",
        "parameters": [
          { "$ref": "#/components/parameters/Owner" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["append"],
                "properties": {
                  "append": { "type": "string", "description": "Anzuhängender Text (wird eigene Zeile)" }
                }
              },
              "example": { "append": "Master -1 dB" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Angehängt – mit vollständigem neuen Text",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/NoteWriteResult" },
                    {
                      "type": "object",
                      "required": ["text"],
                      "properties": {
                        "text": { "type": "string", "description": "Vollständiger Text nach dem Anhängen" }
                      }
                    }
                  ]
                },
                "example": { "name": "Mein Track", "updated_at": "2026-09-03T10:25:12.480Z", "text": "Kick lauter\nVocals ab 1:30\nMaster -1 dB", "owner": { "email": "ich@example.com", "name": "Sebastian" }, "permission": "owner" }
              }
            }
          },
          "400": {
            "description": "`invalid_name`, `invalid_json`, `invalid_body`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_name": { "value": { "error": "invalid_name" } },
                  "invalid_json": { "value": { "error": "invalid_json" } },
                  "invalid_body": { "value": { "error": "invalid_body" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMediaType" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "delete": {
        "tags": ["Notizen"],
        "summary": "Eigene Notiz löschen",
        "description": "Nur der Besitzer. `owner` wird ignoriert; mit mir geteilte (fremde) Notizen → `403 forbidden`. Antwortet auch mit `ok:true`, wenn die Notiz nicht existiert.",
        "operationId": "deleteNote",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/InvalidName" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/notes/{name}/shares": {
      "parameters": [
        { "$ref": "#/components/parameters/NoteName" }
      ],
      "get": {
        "tags": ["Teilen"],
        "summary": "Freigaben einer eigenen Notiz",
        "operationId": "listShares",
        "responses": {
          "200": {
            "description": "Freigaben",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["shares"],
                  "properties": {
                    "shares": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Share" }
                    }
                  }
                },
                "example": {
                  "shares": [
                    { "email": "freund@example.com", "name": "Alex", "permission": "write", "created_at": "2026-09-02T20:00:00.000Z" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidName" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/notes/{name}/shares/{email}": {
      "parameters": [
        { "$ref": "#/components/parameters/NoteName" },
        { "$ref": "#/components/parameters/ShareEmail" }
      ],
      "put": {
        "tags": ["Teilen"],
        "summary": "Freigabe anlegen oder ändern",
        "operationId": "putShare",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["permission"],
                "properties": {
                  "permission": { "type": "string", "enum": ["read", "write"] }
                }
              },
              "example": { "permission": "write" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Freigabe gesetzt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["email", "name", "permission"],
                  "properties": {
                    "email": { "type": "string", "format": "email" },
                    "name": { "type": "string" },
                    "permission": { "type": "string", "enum": ["read", "write"] }
                  }
                },
                "example": { "email": "freund@example.com", "name": "Alex", "permission": "write" }
              }
            }
          },
          "400": {
            "description": "`invalid_name`, `invalid_body`, `cannot_share_self`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_name": { "value": { "error": "invalid_name" } },
                  "invalid_body": { "value": { "error": "invalid_body" } },
                  "cannot_share_self": { "value": { "error": "cannot_share_self" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "`forbidden` – nicht der Besitzer; `quota_exceeded` – schon 50 Freigaben an dieser Notiz",
            "content": {
              "application/json": {
                "schema": { "oneOf": [ { "$ref": "#/components/schemas/Error" }, { "$ref": "#/components/schemas/QuotaError" } ] },
                "examples": {
                  "forbidden": { "value": { "error": "forbidden" } },
                  "quota_exceeded": { "value": { "error": "quota_exceeded", "limit": 50, "what": "shares" } }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Notiz fehlt; `user_not_found` – kein Konto mit dieser E-Mail",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "not_found": { "value": { "error": "not_found" } },
                  "user_not_found": { "value": { "error": "user_not_found" } }
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "delete": {
        "tags": ["Teilen"],
        "summary": "Freigabe entziehen",
        "operationId": "deleteShare",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/InvalidName" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/keys": {
      "get": {
        "tags": ["API-Keys"],
        "summary": "Eigene API-Keys",
        "operationId": "listKeys",
        "responses": {
          "200": {
            "description": "Liste der eigenen Keys (ohne Token, nur `prefix`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["keys"],
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ApiKey" }
                    }
                  }
                },
                "example": {
                  "keys": [
                    { "id": 3, "name": "Ableton Live (MacBook)", "prefix": "smn_9f2c", "created_at": "2026-09-01T18:30:00.000Z", "last_used_at": "2026-09-03T10:25:12.000Z", "current": true },
                    { "id": 4, "name": "n8n", "prefix": "smn_0b7e", "created_at": "2026-09-02T09:00:00.000Z", "last_used_at": null, "current": false }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "post": {
        "tags": ["API-Keys"],
        "summary": "Neuen API-Key erzeugen",
        "description": "**Der Token wird nur dieses eine Mal gezeigt.**",
        "operationId": "createKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "description": "Gerät oder Zweck" }
                }
              },
              "example": { "name": "n8n Studio" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key erzeugt – Token einmalig sichtbar",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiKeyCreated" },
                "example": { "id": 5, "name": "n8n Studio", "token": "smn_…" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/QuotaExceeded" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/keys/{id}": {
      "parameters": [
        { "$ref": "#/components/parameters/KeyId" }
      ],
      "delete": {
        "tags": ["API-Keys"],
        "summary": "API-Key widerrufen",
        "operationId": "deleteKey",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/admin/users": {
      "get": {
        "tags": ["Admin"],
        "summary": "Alle Nutzer",
        "operationId": "adminListUsers",
        "responses": {
          "200": {
            "description": "Nutzerliste mit Anzahl Notizen",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["users"],
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/AdminUser" }
                    }
                  }
                },
                "example": {
                  "users": [
                    { "id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": true, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z", "notes": 12 }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/admin/users/{id}": {
      "parameters": [
        { "$ref": "#/components/parameters/UserId" }
      ],
      "patch": {
        "tags": ["Admin"],
        "summary": "Nutzer ändern (Name, Admin-Status)",
        "operationId": "adminPatchUser",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "is_admin": { "type": "boolean" },
                  "name": { "type": "string" }
                }
              },
              "example": { "is_admin": true }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aktualisierter Nutzer",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UserEnvelope" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/UserNotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "delete": {
        "tags": ["Admin"],
        "summary": "Nutzer löschen (samt Notizen, Keys, Freigaben)",
        "operationId": "adminDeleteUser",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": {
            "description": "`cannot_delete_self` – eigenes Konto kann nicht gelöscht werden",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "cannot_delete_self" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/UserNotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/admin/users/{id}/password": {
      "parameters": [
        { "$ref": "#/components/parameters/UserId" }
      ],
      "post": {
        "tags": ["Admin"],
        "summary": "Passwort eines Nutzers setzen",
        "description": "Alle Sessions des Nutzers enden.",
        "operationId": "adminSetPassword",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["password"],
                "properties": {
                  "password": { "type": "string", "format": "password", "minLength": 8 }
                }
              },
              "example": { "password": "neuesPasswort789" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": {
            "description": "`invalid_body`, `weak_password`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "invalid_body": { "value": { "error": "invalid_body" } },
                  "weak_password": { "value": { "error": "weak_password" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/UserNotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/admin/invites": {
      "get": {
        "tags": ["Admin"],
        "summary": "Alle Einladungen",
        "operationId": "adminListInvites",
        "responses": {
          "200": {
            "description": "Einladungen (offen, abgelaufen, benutzt)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["invites"],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Invite" }
                    }
                  }
                },
                "example": {
                  "invites": [
                    { "id": 7, "email": "neu@example.com", "created_at": "2026-09-03T10:30:00.000Z", "expires_at": "2026-09-10T10:30:00.000Z", "used_at": null }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "post": {
        "tags": ["Admin"],
        "summary": "Einladungslink erzeugen",
        "description": "7 Tage gültig, einmal nutzbar. `url` = `https://notes.secret.vip/?invite=<token>`.",
        "operationId": "adminCreateInvite",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": { "type": "string", "format": "email", "description": "Optional: Einladung an diese Adresse binden" }
                }
              },
              "example": { "email": "neu@example.com" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Einladung erzeugt",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/InviteCreated" },
                "example": { "id": 7, "url": "https://notes.secret.vip/?invite=a1b2c3…", "email": "neu@example.com", "expires_at": "2026-09-10T10:30:00.000Z" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidBody" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/admin/invites/{id}": {
      "parameters": [
        { "$ref": "#/components/parameters/InviteId" }
      ],
      "delete": {
        "tags": ["Admin"],
        "summary": "Einladung löschen",
        "operationId": "adminDeleteInvite",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API-Key: `Authorization: Bearer smn_<hex>`. Für Geräte, Skripte, n8n, fremde Programme. Nie in URLs übertragen."
      },
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "smn_session",
        "description": "Session-Cookie der Website (HttpOnly, 30 Tage). Wird von `POST /api/login` ohne `device` gesetzt. Cross-origin nicht nutzbar."
      }
    },
    "parameters": {
      "NoteName": {
        "name": "name",
        "in": "path",
        "required": true,
        "description": "Notizname = Name des Ableton-Sets. URL-kodiert (Leerzeichen → `%20`). 1–200 Zeichen (Unicode), kein `/`, `\\`, keine Steuerzeichen, kein führender Punkt → sonst `400 invalid_name`.",
        "schema": { "type": "string", "minLength": 1, "maxLength": 200 },
        "example": "Mein Track"
      },
      "ShareEmail": {
        "name": "email",
        "in": "path",
        "required": true,
        "description": "E-Mail des Nutzers, mit dem geteilt wird",
        "schema": { "type": "string", "format": "email" },
        "example": "freund@example.com"
      },
      "UserId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Nutzer-ID",
        "schema": { "type": "integer" },
        "example": 2
      },
      "KeyId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "API-Key-ID",
        "schema": { "type": "integer" },
        "example": 5
      },
      "InviteId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Einladungs-ID",
        "schema": { "type": "integer" },
        "example": 7
      },
      "Owner": {
        "name": "owner",
        "in": "query",
        "required": false,
        "description": "Welche Notiz dieses Namens gemeint ist:\n- `me` → nur meine eigene Notiz\n- `<email>` → die Notiz dieses Nutzers, die mit mir geteilt ist (sonst `404 not_found`)\n- weggelassen → meine eigene, falls vorhanden; sonst eine mit mir geteilte Notiz dieses Namens (die zuletzt geänderte); sonst „existiert nicht“ (gehört mir, wird bei PUT/PATCH angelegt)",
        "schema": { "type": "string" },
        "examples": {
          "me": { "value": "me" },
          "email": { "value": "freund@example.com" }
        }
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "Zuletzt erhaltener `ETag` (z. B. `\"1788430530123\"`). Passt er zum aktuellen Stand → `304 Not Modified` ohne Body.",
        "schema": { "type": "string" },
        "example": "\"1788430530123\""
      }
    },
    "responses": {
      "Ok": {
        "description": "Erfolg",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Ok" },
            "example": { "ok": true }
          }
        }
      },
      "CaptchaRequired": {
        "description": "`captcha_required` – Sicherheitscode nötig, fehlt, falsch oder abgelaufen; neues Bild über `GET /api/captcha` holen",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "captcha_required" } } }
      },
      "TooManyAttempts": {
        "description": "`too_many_attempts` – Rate-Limit erreicht (Login 10 Fehlversuche/15 min/IP, Registrierung 5/h/IP, Passwort vergessen 3/h/E-Mail bzw. 10/h/IP, Captcha 30/10 min/IP)",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "too_many_attempts" } } }
      },
      "QuotaExceeded": {
        "description": "`quota_exceeded` – Kontingent erreicht (1000 Notizen pro Konto, 50 Freigaben pro Notiz, 50 Geräte-Zugänge); Body enthält `limit` und `what`",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuotaError" }, "example": { "error": "quota_exceeded", "limit": 50, "what": "keys" } } }
      },
      "InvalidBody": {
        "description": "`invalid_body` – Pflichtfeld fehlt oder falscher Typ",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "invalid_body" }
          }
        }
      },
      "InvalidName": {
        "description": "`invalid_name` – Notizname verletzt die Namensregel",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "invalid_name" }
          }
        }
      },
      "Unauthorized": {
        "description": "`unauthorized` – kein oder ungültiger Token bzw. keine gültige Session",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "unauthorized" }
          }
        }
      },
      "Forbidden": {
        "description": "`forbidden` – keine Berechtigung (nur-lesen-Freigabe, fremde Notiz, kein Admin)",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "forbidden" }
          }
        }
      },
      "NotFound": {
        "description": "`not_found` – nicht gefunden",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "not_found" }
          }
        }
      },
      "UserNotFound": {
        "description": "`user_not_found` – kein Nutzer mit dieser ID/E-Mail",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "user_not_found" }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "`payload_too_large` – Notiz größer als 1 MB",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "payload_too_large" }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "`unsupported_media_type` – `Content-Type` ist nicht `application/json`",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "unsupported_media_type" }
          }
        }
      },
      "ServerError": {
        "description": "`server_error` – unerwarteter Serverfehler",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "server_error" }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Fehlercode",
            "enum": [
              "invalid_name", "invalid_body", "invalid_json", "weak_password", "cannot_share_self", "cannot_delete_self", "registration_closed", "invalid_invite", "invalid_token",
              "unauthorized", "wrong_credentials",
              "forbidden", "captcha_required", "quota_exceeded",
              "not_found", "user_not_found",
              "conflict", "email_taken",
              "payload_too_large",
              "unsupported_media_type",
              "too_many_attempts",
              "server_error", "mail_unavailable"
            ]
          }
        },
        "example": { "error": "unauthorized" }
      },
      "Ok": {
        "type": "object",
        "required": ["ok"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] }
        }
      },
      "Permission": {
        "type": "string",
        "enum": ["owner", "write", "read"],
        "description": "`owner` = gehört mir, `write` = geteilt mit Schreibrecht, `read` = geteilt, nur lesen"
      },
      "User": {
        "type": "object",
        "required": ["id", "email", "name", "is_admin", "email_verified", "created_at"],
        "properties": {
          "id": { "type": "integer" },
          "email": { "type": "string", "format": "email", "description": "Kleingeschrieben" },
          "name": { "type": "string" },
          "is_admin": { "type": "boolean" },
          "email_verified": { "type": "boolean", "description": "E-Mail-Adresse bestätigt (Bestandskonten, erstes Konto und Einladungen: immer true)" },
          "created_at": { "type": "string", "format": "date-time" }
        },
        "example": { "id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z" }
      },
      "UserEnvelope": {
        "type": "object",
        "required": ["user"],
        "properties": {
          "user": { "$ref": "#/components/schemas/User" }
        }
      },
      "CaptchaAnswer": {
        "type": "object",
        "required": ["id", "answer"],
        "properties": {
          "id": { "type": "string", "description": "`id` aus `GET /api/captcha`" },
          "answer": { "type": "string", "description": "Die Zeichen aus dem Bild (Groß-/Kleinschreibung egal)" }
        },
        "example": { "id": "0f3a…", "answer": "k7mpx" }
      },
      "Captcha": {
        "type": "object",
        "required": ["id", "svg", "ttl"],
        "properties": {
          "id": { "type": "string", "description": "Einmal-Kennung, 10 Minuten gültig" },
          "svg": { "type": "string", "description": "Bild als SVG-Markup (180×60)" },
          "ttl": { "type": "integer", "description": "Gültigkeit in Sekunden", "example": 600 }
        }
      },
      "QuotaError": {
        "type": "object",
        "required": ["error", "limit", "what"],
        "properties": {
          "error": { "type": "string", "enum": ["quota_exceeded"] },
          "limit": { "type": "integer" },
          "what": { "type": "string", "enum": ["notes", "shares", "keys"] }
        }
      },
      "LoginResult": {
        "type": "object",
        "required": ["user"],
        "properties": {
          "token": { "type": "string", "description": "Nur mit `device`: neuer API-Key `smn_…`", "example": "smn_…" },
          "user": { "$ref": "#/components/schemas/User" }
        }
      },
      "RegisterInfo": {
        "type": "object",
        "required": ["open", "first_user", "captcha_required", "password_reset", "email_verification", "min_password"],
        "properties": {
          "open": { "type": "boolean", "description": "Server offen für Registrierung (`SMN_OPEN_REGISTRATION=1`)" },
          "first_user": { "type": "boolean", "description": "Noch kein Nutzer vorhanden – der erste wird Admin" },
          "captcha_required": { "type": "boolean", "description": "Registrierung braucht `captcha` (nicht beim ersten Konto, nicht mit gültiger Einladung)" },
          "password_reset": { "type": "boolean", "description": "`POST /api/password/forgot` verfügbar (E-Mail-Versand eingerichtet)" },
          "email_verification": { "type": "boolean", "description": "Neue Konten müssen die E-Mail-Adresse bestätigen (Bestätigungs-Mail, `POST /api/register/verify`); `false` nur ohne E-Mail-Versand – dann sind neue Konten sofort aktiv" },
          "min_password": { "type": "integer", "description": "Mindestlänge des Passworts", "example": 8 },
          "invite_valid": { "type": "boolean", "description": "Nur mit `?invite=`: Token gültig" },
          "invite_email": { "type": "string", "format": "email", "nullable": true, "description": "Nur mit `?invite=`: gebundene E-Mail oder `null`" }
        }
      },
      "Owner": {
        "type": "object",
        "required": ["email", "name"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "name": { "type": "string" }
        },
        "example": { "email": "ich@example.com", "name": "Sebastian" }
      },
      "NoteListItem": {
        "type": "object",
        "required": ["name", "owner", "mine", "permission", "updated_at", "size"],
        "properties": {
          "name": { "type": "string" },
          "owner": { "$ref": "#/components/schemas/Owner" },
          "mine": { "type": "boolean", "description": "Gehört die Notiz mir?" },
          "permission": { "$ref": "#/components/schemas/Permission" },
          "updated_at": { "type": "string", "format": "date-time" },
          "size": { "type": "integer", "description": "Größe des Textes in Bytes" }
        }
      },
      "Note": {
        "type": "object",
        "required": ["name", "text", "updated_at", "exists", "owner", "mine", "permission"],
        "properties": {
          "name": { "type": "string" },
          "text": { "type": "string", "description": "Leer, wenn `exists:false`" },
          "updated_at": { "type": "string", "format": "date-time", "nullable": true, "description": "`null`, wenn `exists:false`" },
          "exists": { "type": "boolean" },
          "owner": { "$ref": "#/components/schemas/Owner" },
          "mine": { "type": "boolean" },
          "permission": { "$ref": "#/components/schemas/Permission" }
        }
      },
      "NoteWriteResult": {
        "type": "object",
        "required": ["name", "updated_at", "owner", "permission"],
        "properties": {
          "name": { "type": "string" },
          "updated_at": { "type": "string", "format": "date-time" },
          "owner": { "$ref": "#/components/schemas/Owner" },
          "permission": { "$ref": "#/components/schemas/Permission" }
        }
      },
      "Conflict": {
        "type": "object",
        "description": "Antwort bei `409 conflict`: der aktuelle Serverstand der Notiz",
        "required": ["error", "name", "text", "updated_at", "owner", "permission"],
        "properties": {
          "error": { "type": "string", "enum": ["conflict"] },
          "name": { "type": "string" },
          "text": { "type": "string", "description": "Aktueller Text auf dem Server (`\"\"`, wenn die Notiz fehlt)" },
          "updated_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Aktueller Stand (`null`, wenn die Notiz fehlt) – als neues `base_updated_at` verwenden" },
          "owner": { "$ref": "#/components/schemas/Owner" },
          "permission": { "$ref": "#/components/schemas/Permission" }
        }
      },
      "Share": {
        "type": "object",
        "required": ["email", "name", "permission", "created_at"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "name": { "type": "string" },
          "permission": { "type": "string", "enum": ["read", "write"] },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": ["id", "name", "prefix", "created_at", "last_used_at", "current"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "prefix": { "type": "string", "description": "Anfang des Tokens zur Wiedererkennung", "example": "smn_9f2c" },
          "created_at": { "type": "string", "format": "date-time" },
          "last_used_at": { "type": "string", "format": "date-time", "nullable": true },
          "current": { "type": "boolean", "description": "Der Key, mit dem dieser Aufruf gemacht wurde" }
        }
      },
      "ApiKeyCreated": {
        "type": "object",
        "required": ["id", "name", "token"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "token": { "type": "string", "description": "Wird nur dieses eine Mal gezeigt", "example": "smn_…" }
        }
      },
      "AdminUser": {
        "allOf": [
          { "$ref": "#/components/schemas/User" },
          {
            "type": "object",
            "required": ["notes"],
            "properties": {
              "notes": { "type": "integer", "description": "Anzahl Notizen des Nutzers" }
            }
          }
        ]
      },
      "Invite": {
        "type": "object",
        "required": ["id", "email", "created_at", "expires_at", "used_at"],
        "properties": {
          "id": { "type": "integer" },
          "email": { "type": "string", "format": "email", "nullable": true, "description": "Gebundene E-Mail oder `null`" },
          "created_at": { "type": "string", "format": "date-time" },
          "expires_at": { "type": "string", "format": "date-time", "description": "7 Tage nach Erstellung" },
          "used_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "InviteCreated": {
        "type": "object",
        "required": ["id", "url", "email", "expires_at"],
        "properties": {
          "id": { "type": "integer" },
          "url": { "type": "string", "format": "uri", "description": "`https://notes.secret.vip/?invite=<token>`" },
          "email": { "type": "string", "format": "email", "nullable": true },
          "expires_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  }
}
