openapi: 3.1.0
info:
  title: Vetora API
  version: "1.0.0"
  description: >
    API pour lancer un scan de sécurité et récupérer le résultat en une seule
    requête — pensée pour bloquer un déploiement en CI/CD si le score de
    sécurité n'est pas suffisant. Pas de compte utilisateur : une clé API est
    simplement rattachée à une adresse e-mail.
  contact:
    email: assyamekhanteur@gmail.com
servers:
  - url: https://www.vetora.site
tags:
  - name: API keys
    description: Émission et gestion des clés API (pas de compte, une clé par e-mail).
  - name: Scans
    description: Lancer un scan de sécurité et récupérer son résultat.

paths:
  /api/api-keys:
    post:
      tags: [API keys]
      summary: Créer une clé API
      description: >
        Aucune authentification requise pour cet appel — c'est lui qui délivre
        la clé. La valeur brute de la clé n'est renvoyée qu'une seule fois,
        dans cette réponse ; seul son hash est conservé côté serveur.
        Limité à 3 créations par adresse IP toutes les 24h. Maximum 5 clés
        actives simultanément par e-mail.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
            examples:
              default:
                value:
                  email: you@example.com
                  label: CI GitHub Actions
                  organization: acme-inc
                  project: web-app
                  expiresInDays: 90
      responses:
        '200':
          description: Clé créée.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        '400':
          description: E-mail, label, organization, project ou expiresInDays invalide.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Maximum de 5 clés actives déjà atteint pour cet e-mail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Plus de 3 clés créées depuis cette IP dans les dernières 24h.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Erreur interne.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/scan:
    post:
      tags: [Scans]
      summary: Lancer un scan et récupérer le résultat
      security:
        - bearerAuth: []
      description: >
        Requête synchrone : bloque jusqu'à la fin du scan (généralement
        quelques secondes, jusqu'à ~30s selon le site). Le site scanné doit
        être publiquement accessible au moment de l'appel. Limité à 5 scans
        par clé API toutes les 10 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanRequest'
            examples:
              default:
                value:
                  url: https://your-app.vercel.app
                  minScore: B
      responses:
        '200':
          description: Scan terminé.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        '400':
          description: URL manquante/invalide, ou minScore invalide.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Clé API manquante, mal formée, invalide, révoquée ou expirée.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Plus de 5 scans lancés avec cette clé dans les 10 dernières minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Erreur interne (création du scan, vérification de la clé).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Site scanné inaccessible ou timeout pendant le scan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "vs_live_xxx"
      description: >
        Clé obtenue via POST /api/api-keys, envoyée en en-tête
        `Authorization: Bearer vs_live_xxx`.

  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Message d'erreur destiné à l'humain (en français).
      required: [error]

    CreateApiKeyRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        label:
          type: string
          maxLength: 100
          nullable: true
          description: Label libre pour identifier la clé (ex. "CI GitHub Actions").
        organization:
          type: string
          maxLength: 100
          nullable: true
          description: Label libre pour grouper des clés.
        project:
          type: string
          maxLength: 100
          nullable: true
          description: Label libre pour grouper des clés.
        expiresInDays:
          type: integer
          minimum: 1
          maximum: 3650
          nullable: true
          description: Omis (ou null) = la clé n'expire jamais.

    CreateApiKeyResponse:
      type: object
      properties:
        apiKey:
          type: string
          description: Valeur brute de la clé — affichée une seule fois, ici.
          example: "vs_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
        email:
          type: string
        label:
          type: string
          nullable: true
        organization:
          type: string
          nullable: true
        project:
          type: string
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        managementUrl:
          type: string
          format: uri
          description: >
            Lien de gestion personnel (lister/révoquer/régénérer les clés de
            cet e-mail), aussi envoyé par e-mail.
        warning:
          type: string

    ScanRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          example: "https://your-app.vercel.app"
        minScore:
          type: string
          enum: [A, B, C, D, F]
          nullable: true
          description: >
            Optionnel. Si présent, la réponse indique si le score atteint ce
            seuil via `passed`. Sans lui, `passed` vaut `null`.

    Finding:
      type: object
      properties:
        title:
          type: string
        severity:
          type: string
          enum: [critical, high, medium, low, info]
        description:
          type: string
        fixInstructions:
          type: string
        evidence:
          type: string
          description: Extrait technique brut appuyant le constat (ex. en-têtes HTTP observés).
        confidence:
          type: string
          enum: [confirmed, likely, possible]

    ScanResponse:
      type: object
      properties:
        scanId:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        score:
          type: string
          enum: [A, B, C, D, F]
        minScore:
          type: string
          enum: [A, B, C, D, F]
          nullable: true
        passed:
          type: boolean
          nullable: true
          description: "`null` si `minScore` n'a pas été fourni dans la requête."
        detectedBuilder:
          type: string
          enum: [bolt, lovable]
          nullable: true
          description: >
            Détection automatique et informative (pas une faille), basée sur
            le badge d'attribution public de la plateforme. v0 et Cursor ne
            sont pas détectés : aucun badge équivalent à chercher.
        reportUrl:
          type: string
          format: uri
        findings:
          type: object
          properties:
            base:
              type: array
              description: >
                Failles de la catégorie "Sécurité de base" (headers, HTTPS,
                cookies, CORS) — détaillées en clair, gratuites.
              items:
                $ref: '#/components/schemas/Finding'
            premium:
              type: object
              description: >
                Comptées mais non détaillées, même règle qu'un scan non payé
                sur le site — l'API ne contourne pas le rapport complet payant.
              properties:
                stripeAndSupabaseCount:
                  type: integer
                vibeCodingCount:
                  type: integer
                infrastructureCount:
                  type: integer
                unlockUrl:
                  type: string
                  format: uri

    AlertWebhookPayload:
      type: object
      description: >
        Corps envoyé par Vetora vers le webhook générique configuré sur un
        abonnement surveillé (POST /api/alert-channels), signé en en-tête
        `X-Vetora-Signature` — voir la section Webhooks de la documentation.
        Ceci n'est PAS un endpoint de cette API : c'est le format que votre
        propre récepteur doit accepter.
      properties:
        event:
          type: string
          const: security_alert
        subscriptionId:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        reportUrl:
          type: string
          format: uri
        previousScore:
          type: string
          enum: [A, B, C, D, F]
          nullable: true
        currentScore:
          type: string
          enum: [A, B, C, D, F]
          nullable: true
        scoreDropped:
          type: boolean
        regressedFindings:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              severity:
                type: string
        newCriticalFindings:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              severity:
                type: string
        fixedCount:
          type: integer
        timestamp:
          type: string
          format: date-time
