openapi: 3.1.0
info:
  title: KeyVault API
  version: "1.0.0"
  summary: Coffre-fort de secrets pour applications, accessible par API.
  description: |
    KeyVault centralise, chiffre (AEAD ChaCha20-Poly1305) et fait tourner les
    clés API et mots de passe de vos applications.

    **Authentification à deux niveaux :**

    1. `POST /api/v1/auth` échange un `client_id` / `client_secret` (niveau 1)
       contre un **JWT HS256 éphémère** (niveau 2), valable **300 secondes**.
    2. Les autres requêtes portent ce jeton dans l'en-tête
       `Authorization: Bearer <JWT>`. Le serveur valide signature et expiration
       en mémoire, sans accès base.

    Le `client_id` / `client_secret` s'obtiennent depuis l'espace client
    (https://app.keyvaultapi.newfutur.com) pour chaque application déclarée.
  contact:
    name: NEWFUTUR
    url: https://keyvaultapi.newfutur.com/
  license:
    name: Propriétaire

servers:
  - url: https://api.keyvaultapi.newfutur.com
    description: Production

tags:
  - name: Authentification
  - name: Secrets
  - name: Service

paths:
  /api/v1/auth:
    post:
      tags: [Authentification]
      summary: Émettre un jeton d'accès
      description: |
        Niveau 1. En cas d'échecs répétés (par `client_id` ou par IP), le
        endpoint applique un ralentissement exponentiel et répond `429`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, client_secret]
              properties:
                client_id:
                  type: string
                  example: kvid_9Z1pXe0m4Qb7Rk2Ns6Td8Uv3Wy5A
                client_secret:
                  type: string
                  example: kvsec_3f8b2c9d1e0a7654bb21cc09ffee5533aa11b2c3d4e5f6071
      responses:
        "200":
          description: Jeton émis
          content:
            application/json:
              schema:
                type: object
                properties:
                  token_type: { type: string, const: Bearer }
                  access_token: { type: string }
                  expires_in: { type: integer, example: 300 }
                  scopes:
                    type: array
                    items: { type: string }
                    example: [secret:read, secret:write]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          description: Trop de tentatives
          headers:
            Retry-After:
              schema: { type: integer }
              description: Délai en secondes avant nouvelle tentative
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/v1/secrets/{secret_key}:
    parameters:
      - name: secret_key
        in: path
        required: true
        schema: { type: string, pattern: "^kvsk_[0-9A-Za-z]{32}$" }
        example: kvsk_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56
    get:
      tags: [Secrets]
      summary: Lire la valeur d'un secret
      security:
        - bearerAuth: [secret:read]
      responses:
        "200":
          description: Valeur déchiffrée
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret_key: { type: string }
                  value: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Secrets]
      summary: Mettre à jour la valeur d'un secret
      description: |
        L'ancienne valeur est archivée (journal immuable). Un nouveau nonce est
        généré et la version est incrémentée.
      security:
        - bearerAuth: [secret:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:
                  type: string
                  maxLength: 4000
                  description: Nouvelle valeur en clair (1 à 4000 octets)
      responses:
        "200":
          description: Secret mis à jour
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret_key: { type: string }
                  value_version: { type: integer, example: 4 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /api/v1/health:
    get:
      tags: [Service]
      summary: Sonde de disponibilité
      security: []
      responses:
        "200":
          description: Service opérationnel
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, const: ok }
                  time: { type: string, format: date-time }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT HS256 obtenu via `POST /api/v1/auth`. Durée de vie 300 s. Portées
        possibles : `secret:read`, `secret:write`.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Code d'erreur stable (snake_case)
          examples: [invalid_credentials, invalid_token, insufficient_scope, not_found, rate_limited, invalid_request]
      required: [error]
    ValidationError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            fields:
              type: object
              additionalProperties: { type: string }

  responses:
    Unauthorized:
      description: Identifiants ou jeton invalides / expirés
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Portée (scope) insuffisante
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Secret introuvable pour l'application authentifiée
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ValidationError:
      description: Corps de requête invalide
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ValidationError" }
