openapi: 3.0.3
info:
  title: AIActify API
  version: 1.1.0
  description: >
    EU AI Act Compliance Platform.
    Achtung: Diese Datei deckt Sites, Content, Audit, Auth, Scripts, Agents,
    Inventar und Snippet ab. NICHT enthalten sind API-Keys, PDF-Reports,
    Rules-Engine, KI-Schulung, Notifications und WordPress — dafuer ist
    docs/api.html die genauere Quelle.
  contact:
    email: info@aiactify.eu

servers:
  - url: https://aiactify-backend.vercel.app/api
    description: Production
  - url: http://localhost:3001/api
    description: Local

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }

    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        company: { type: string }
        plan: { type: string, enum: [trial, starter, pro, agency, enterprise] }
        email_verified: { type: boolean }
        created_at: { type: string, format: date-time }

    Site:
      type: object
      properties:
        id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        domain: { type: string }
        name: { type: string }
        site_key: { type: string }
        settings:
          type: object
          description: 'Visual configuration only. Compliance features (schemaOrg, scriptAuditor, piiProtection) are always active per EU AI Act and not configurable.'
          properties:
            labelStyle: { type: string, enum: [badge, minimal, verbose] }
            labelPosition: { type: string, enum: [bottom-right, bottom-left, top-right, top-left] }
            brandingColor: { type: string, example: '#3ECF8E' }
            logoUrl: { type: string, format: uri }
            complianceBadge:
              type: object
              description: 'Optional public compliance badge shown on the customer website. Opt-in only.'
              properties:
                enabled: { type: boolean, default: false, description: 'User must explicitly opt in' }
        created_at: { type: string, format: date-time }

    SitePage:
      type: object
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        parent_id: { type: string, format: uuid, nullable: true, description: 'For hierarchical folder structure' }
        url: { type: string, format: uri }
        path: { type: string, description: 'Relative path, e.g., /about/team' }
        title: { type: string }
        thumbnail_url: { type: string, format: uri, nullable: true, description: 'Screenshot generated by crawler' }
        is_imported: { type: boolean, description: 'True if user selected it from crawler results to be monitored' }
        created_at: { type: string, format: date-time }

    ContentItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        content_hash: { type: string }
        content_type: { type: string, enum: [text, image, video, audio] }
        url: { type: string }
        selector: { type: string }
        preview: { type: string }
        status: { type: string, enum: [pending, ai, human] }
        ai_confidence: { type: number, minimum: 0, maximum: 1 }
        is_exempt: { type: boolean }
        exempt_reason: { type: string, enum: [artistic, satirical, fictional, editorial_control, obvious_ai, out_of_scope] }
        validated_at: { type: string, format: date-time }
        detected_at: { type: string, format: date-time }

    AuditLog:
      type: object
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        action: { type: string }
        details: { type: object }
        integrity_hash: { type: string, description: "Keyed HMAC-SHA256 ('h2:' prefix) or legacy SHA-256" }
        prev_hash: { type: string, nullable: true, description: 'Hash chain: integrity_hash of the previous entry' }
        ip_address: { type: string }
        created_at: { type: string, format: date-time }

    PlanLimits:
      type: object
      properties:
        plan: { type: string }
        maxSites: { type: integer }
        contentLimit: { type: integer, nullable: true, description: 'null = unlimited' }

    # ── Art. 5 Detection: Source of Truth ──────────────────────────────────────
    # These schemas define ALL detection rules for the enforcer snippet.
    # When adding new patterns, update here FIRST, then sync scripts.js.

    ForbiddenPattern:
      type: object
      required: [id, name, article, severity, action, patterns]
      properties:
        id: { type: string, description: 'Unique rule identifier' }
        name: { type: string, description: 'Human-readable category name' }
        article: { type: string, description: 'EU AI Act article reference' }
        severity: { type: string, enum: [critical, high, medium] }
        action:
          type: string
          enum: [block, warn, monitor]
          description: |
            Client-side enforcement level:
            * block   — request/script blocked + visitor banner. Reserved for
                        unambiguous Art.-5 services (e.g. face-scraping DBs).
            * warn    — reported to dashboard + operator e-mail. No visitor
                        banner, no blocking (blocking is opt-in per finding).
            * monitor — reported for review. Dual-use SDKs/cloud APIs with
                        many lawful uses; context must be assessed by the
                        operator (Art. 5 vs. Art. 50(3) disclosure duty).
        description: { type: string }
        patterns:
          type: array
          description: 'URL substrings to match against script src attributes'
          items: { type: string }
        apiEndpoints:
          type: array
          description: 'Known API endpoint patterns to intercept in fetch/XHR'
          items: { type: string }

    ForbiddenPatternRegistry:
      type: array
      description: |
        Master registry of all forbidden AI practice detection rules.
        Enforcer snippet uses these to audit scripts AND network requests.
        Ordered by article number.
      items:
        $ref: '#/components/schemas/ForbiddenPattern'
      example:
        # ── Art. 5(1)(a): Subliminal Manipulation ─────────────────────────
        - id: subliminal_manipulation
          name: Unterschwellige Manipulation / Subliminal Techniques
          article: 'Art. 5(1)(a)'
          severity: high
          action: warn
          description: 'AI techniques that manipulate persons beyond their consciousness'
          patterns:
            - subliminal
            - neuromarketing
            - neurometric
            - brain-computer
            - bci-sdk
            - neurofeedback
            - eeg-analytics
          apiEndpoints:
            - api.neurosity.co
            - api.emotiv.com

        # ── Art. 5(1)(b): Exploiting Vulnerabilities ──────────────────────
        # NOTE: marketing-generic terms (age-targeting, engagement-maximize)
        # were removed — false-positive rate on ordinary ad-tech was too high.
        - id: vulnerability_exploitation
          name: Ausnutzung von Schwächen / Vulnerability Exploitation
          article: 'Art. 5(1)(b)'
          severity: high
          action: warn
          description: 'AI exploiting age, disability, or social/economic situation'
          patterns:
            - vulnerability-score
            - disability-detect
            - cognitive-load
            - attention-hijack
          apiEndpoints: []

        # ── Art. 5(1)(c): Social Scoring ──────────────────────────────────
        - id: social_scoring
          name: Social Scoring
          article: 'Art. 5(1)(c)'
          severity: critical
          action: warn
          description: 'AI-based social scoring leading to detrimental treatment'
          patterns:
            - social-score
            - social-scoring
            - trustworthiness-score
            - citizen-score
            - behavior-score
            - reputation-score
            - credit-social
            - trust-rating
          apiEndpoints: []

        # ── Art. 5(1)(d): Predictive Policing ─────────────────────────────
        # NOTE: 'risk-assessment-ai' removed (generic term, e.g. finance/health).
        - id: predictive_policing
          name: Predictive Policing
          article: 'Art. 5(1)(d)'
          severity: critical
          action: warn
          description: 'Risk assessment of individuals based on profiling for crime prediction'
          patterns:
            - predictive-policing
            - crime-prediction
            - recidivism
            - threat-scoring
          apiEndpoints: []

        # ── Art. 5 (Digital Omnibus): Non-consensual intimate imagery / CSAM ─
        # New prohibited practice added by the Digital Omnibus (transition
        # period until 2 Dec 2026). warn + dashboard-opt-in blocking, in line
        # with the cautious default (avoids false positives breaking sites).
        - id: nonconsensual_intimate_imagery
          name: KI-Nacktbild-/Deepfake-Missbrauch
          article: 'Art. 5 (Digital Omnibus)'
          severity: critical
          action: warn
          description: 'AI generating non-consensual intimate imagery or CSAM ("nudifier"/"undress" apps)'
          patterns:
            - nudify
            - nudifier
            - deepnude
            - undress-ai
            - undressai
            - clothoff
            - deepnude-api
            - nsfw-generator
            - undress-app
            - ai-undress
          apiEndpoints: []

        # ── Art. 5(1)(e): Facial Recognition Databases ────────────────────
        # Only category with action=block: these services are unambiguous
        # Art.-5 violations with no lawful client-side use case.
        - id: facial_recognition_scraping
          name: Gesichtserkennung aus Internet-Scraping
          article: 'Art. 5(1)(e)'
          severity: critical
          action: block
          description: 'Untargeted scraping of facial images from the internet/CCTV'
          patterns:
            - clearview
            - pimeyes
            - findface
            - facecheck
          apiEndpoints:
            - api.clearview.ai
            - api.pimeyes.com

        # ── Art. 5(1)(f) / Art. 50(3): Emotion Recognition ────────────────
        # Prohibited ONLY in workplace/education (Art. 5(1)(f)); elsewhere
        # permitted subject to Art. 50(3) disclosure. Context cannot be
        # determined by pattern matching → warn, operator assesses.
        - id: emotion_recognition
          name: Emotionserkennung / Emotion Recognition
          article: 'Art. 5(1)(f) / Art. 50(3)'
          severity: high
          action: warn
          description: 'Emotion inference: prohibited in workplace/education, disclosure duty elsewhere'
          patterns:
            # Dedicated emotion-analysis vendors/SDKs
            - affectiva
            - emotient
            - kairos
            - realeyes
            - morphcast
            - visage-sdk
            - facereader
            - noldus
            # Emotion-specific SDK patterns
            - emotion-detection
            - emotion-recognition
            - emotion-api
            - emotion-analysis
            - facial-emotion
            - face-emotion
            - sentiment-face
            - affect-recognition
            - micro-expression
            - expression-analysis
          apiEndpoints:
            - api.kairos.com
            - api.affectiva.com
            - api.realeyes.com
            - api.morphcast.com

        # ── Art. 5(1)(g) / Art. 50(3): Biometric Categorization ───────────
        # NOTE: fingerprint-js/fingerprintjs removed (device fingerprinting,
        # not biometrics); biometric-auth removed (authentication is lawful).
        - id: biometric_categorization
          name: Biometrische Kategorisierung / Biometric Categorization
          article: 'Art. 5(1)(g) / Art. 50(3)'
          severity: high
          action: warn
          description: 'Biometric categorization: prohibited for sensitive attributes, disclosure duty otherwise'
          patterns:
            # Vendors with categorization capabilities
            - deepface
            - faceplusplus
            - megvii
            - sensetime
            # Biometric-specific
            - biometric-categoriz
            - biometric-classif
            - iris-recognition
            - gait-analysis
            - voice-biometric
            - speaker-identification
          apiEndpoints:
            - api.faceplusplus.com
            - api-us.faceplusplus.com
            - api.luxand.cloud
            - api.skybiometry.com

        # ── Art. 5(1)(h): Real-time Biometric Identification ──────────────
        - id: realtime_biometric_id
          name: Echtzeit-Biometrische Identifizierung
          article: 'Art. 5(1)(h)'
          severity: critical
          action: warn
          description: 'Real-time remote biometric identification in publicly accessible spaces'
          patterns:
            - realtime-face
            - live-face-recognition
            - face-stream
            - surveillance-ai
            - crowd-analysis
            - people-tracking
            - person-reid
          apiEndpoints: []

        # ── Art. 50(3) (context-dependent): Generic Face Analysis ─────────
        # Dual-use SDKs / cloud vision APIs with many lawful uses
        # (AR filters, autofocus, accessibility, moderation, auth).
        # Monitor only — the operator assesses the deployment context.
        - id: face_analysis_review
          name: Gesichtsanalyse (Prüfung erforderlich) / Face Analysis (review)
          article: 'Art. 50(3) (kontextabhängig)'
          severity: medium
          action: monitor
          description: 'Generic face detection SDKs and cloud vision APIs — review use case in dashboard'
          patterns:
            - face-api
            - faceapi
            - face-landmarks
            - tensorflow-face
            - mediapipe-face
            - face-detect
            - face-recognition
            - facial-recognition
            - aws-rekognition
            - azure-face
          apiEndpoints:
            - face.cognitiveservices.azure.com
            - rekognition.amazonaws.com
            - vision.googleapis.com/v1/images:annotate

    BrowserPermissionMonitor:
      type: object
      description: |
        Browser APIs to monitor for potential Art. 5 violations.
        The enforcer intercepts these API calls and reports them.
      properties:
        camera:
          type: object
          properties:
            api: { type: string, example: 'navigator.mediaDevices.getUserMedia({video: true})' }
            risk: { type: string, example: 'Potential emotion recognition or biometric capture' }
            article: { type: string, example: 'Art. 5(1)(f), Art. 5(1)(g)' }
        microphone:
          type: object
          properties:
            api: { type: string, example: 'navigator.mediaDevices.getUserMedia({audio: true})' }
            risk: { type: string, example: 'Potential voice biometric analysis' }
            article: { type: string, example: 'Art. 5(1)(g)' }
        canvasFingerprint:
          type: object
          properties:
            api: { type: string, example: 'canvas.toDataURL() / canvas.getContext("2d")' }
            risk: { type: string, example: 'Device fingerprinting for tracking/scoring' }
            article: { type: string, example: 'Art. 5(1)(c)' }


tags:
  - name: Auth
    description: Authentication & user management
  - name: Sites
    description: Website management
  - name: Content
    description: AI content items
  - name: Audit
    description: Immutable audit logs
  - name: Stripe
    description: Payments & subscriptions
  - name: Scripts
    description: Script auditor findings
  - name: Agents
    description: AI agent disclosures
  - name: Snippet
    description: Client-side enforcer script

paths:
  # ── Health ──────────────────────────────────────────────────────────────────
  /health:
    get:
      tags: [Auth]
      summary: Health check
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  database: { type: string, example: connected }

  # ── Auth ─────────────────────────────────────────────────────────────────────
  /auth/register:
    post:
      tags: [Auth]
      summary: Register new user
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                company: { type: string }
                startTrial: { type: boolean, default: false }
      responses:
        '201':
          description: User created — verification email sent
        '409':
          description: Email already registered
        '400':
          $ref: '#/components/schemas/Error'

  /auth/login:
    post:
      tags: [Auth]
      summary: Login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
      responses:
        '200':
          description: JWT token returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  user: { $ref: '#/components/schemas/User' }
        '401':
          description: Invalid credentials or email not verified

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current user
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }

  /auth/verify-email:
    post:
      tags: [Auth]
      summary: Verify email with token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }

  /auth/resend-verification:
    post:
      tags: [Auth]
      summary: Resend verification email (authenticated)

  /auth/resend-verification-by-email:
    post:
      tags: [Auth]
      summary: Resend verification email (unauthenticated)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }

  /auth/forgot-password:
    post:
      tags: [Auth]
      summary: Send password reset email
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }

  /auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password with token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, newPassword]
              properties:
                token: { type: string }
                newPassword: { type: string, minLength: 8 }

  /auth/change-password:
    post:
      tags: [Auth]
      summary: Change password (requires current password)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword: { type: string }
                newPassword: { type: string, minLength: 8 }

  /auth/account:
    delete:
      tags: [Auth]
      summary: Delete account and all data (GDPR Art. 17)
      description: |
        Irreversible self-service account deletion. Requires the account
        password for confirmation. Cancels any active Stripe subscription and
        deletes the Stripe customer first (a failure here aborts the deletion),
        then removes the user and — via ON DELETE CASCADE — all sites, content
        items, rules, findings, site pages, API keys and auth tokens, plus the
        account's audit logs and their archive. A confirmation email is sent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, description: Current account password }
      responses:
        '200':
          description: Account and all associated data deleted
        '400':
          description: Password missing
        '401':
          description: Password incorrect
        '502':
          description: Subscription cancellation failed — account NOT deleted

  /auth/onsite-editor-token:
    post:
      tags: [Auth]
      summary: Generate token for Visual On-Site Editor
      description: |
        Generates a short-lived, secure token. This can be called using a standard user JWT (dashboard) OR an API Key (e.g. from the WordPress plugin).
        The response token is passed to the client's site via URL (e.g. `?aiactify_edit=TOKEN`). The injected snippet verifies it to activate the Visual Editor widget.
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  editorToken: { type: string }
                  expiresAt: { type: string, format: date-time }

  /auth/notification-settings:
    get:
      tags: [Auth]
      summary: Get notification preferences
    put:
      tags: [Auth]
      summary: Update notification preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email_notifications: { type: boolean }
                weekly_report: { type: boolean }
                alert_on_violation: { type: boolean }

  /auth/verify-editor-token:
    post:
      tags: [Auth, Snippet]
      summary: Verify Visual Editor token from enforcer snippet
      description: |
        Called by the enforcer.js snippet when loaded with an editor token.
        Verifies the token and returns site config for editor mode.
        Public endpoint — called from customer websites (any origin).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        '200':
          description: Token valid — editor config returned
        '401':
          description: Invalid or expired token

  # ── Sites ────────────────────────────────────────────────────────────────────
  /sites:
    get:
      tags: [Sites]
      summary: List all sites for user
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  sites:
                    type: array
                    items: { $ref: '#/components/schemas/Site' }
                  plan: { type: string }
                  maxSites: { type: integer }
                  contentLimit: { type: integer, nullable: true }
    post:
      tags: [Sites]
      summary: Create new site
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string }
                name: { type: string }
                settings: { type: object }
      responses:
        '201':
          description: Site created
        '403':
          description: Site limit reached (SITE_LIMIT_REACHED)

  /sites/{id}:
    get:
      tags: [Sites]
      summary: Get single site
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
    put:
      tags: [Sites]
      summary: Update site settings / name / domain
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                domain: { type: string }
                settings: { $ref: '#/components/schemas/Site/properties/settings' }
    delete:
      tags: [Sites]
      summary: Delete site and all associated data
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }

  /sites/{siteId}/pages:
    get:
      tags: [Sites]
      summary: Get all pages/subsites for a site (hierarchical tree)
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/SitePage' }
    post:
      tags: [Sites]
      summary: Manually add a page/subsite
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                parent_id: { type: string, format: uuid, nullable: true }

  /sites/{siteId}/pages/{pageId}:
    put:
      tags: [Sites]
      summary: Update page (e.g., mark as imported/monitored)
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: pageId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_imported: { type: boolean }
                parent_id: { type: string, format: uuid, nullable: true }
    delete:
      tags: [Sites]
      summary: Remove page
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: pageId
          in: path
          required: true
          schema: { type: string, format: uuid }

  /sites/{siteId}/crawl:
    post:
      tags: [Sites]
      summary: Trigger site crawler to discover subpages and generate thumbnails
      description: Starts a background Puppeteer job that crawls the sitemap/navigation, discovers all subpages, takes a small screenshot of each, and populates the SitePage list with is_imported=false.
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '202':
          description: Crawl job started

  /sites/badge/{siteKey}:
    get:
      tags: [Sites]
      summary: Get public compliance badge for a site
      description: >
        Public endpoint (no auth required). Returns an SVG compliance badge
        for the given site. Returns 404 if the site has not opted in
        (complianceBadge.enabled = false).
      security: []
      parameters:
        - name: siteKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: SVG badge
          content:
            image/svg+xml:
              schema: { type: string }
        '404':
          description: Badge not enabled for this site
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /sites/config/{siteKey}:
    get:
      tags: [Sites]
      summary: Get site config (public — used by snippet)
      security: []
      parameters:
        - name: siteKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Site configuration for enforcer snippet
          headers:
            Cache-Control:
              schema: { type: string, example: 'public, s-maxage=60, stale-while-revalidate=300' }

  /sites/init/{siteKey}:
    post:
      tags: [Sites, Snippet]
      summary: Combined init — config + content register + statuses + agents in one call
      description: |
        Performance-optimized endpoint that replaces 4 sequential API calls with a single request.
        Called by the enforcer snippet on page load. Returns everything needed to initialize:
        1. Site config (same as /sites/config/{siteKey})
        2. Content registration results (same as /content/register)
        3. Content statuses (same as /content/status/{siteKey})
        4. Agent disclosures (same as /agents/config/{siteKey})

        Reduces badge display latency from ~3-5s to ~200-400ms.
      security: []
      parameters:
        - name: siteKey
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  maxItems: 50
                  description: Pre-scanned content items from the page DOM
                  items:
                    type: object
                    properties:
                      hash: { type: string }
                      type: { type: string, enum: [text, image, video, audio] }
                      url: { type: string }
                      selector: { type: string }
                      preview: { type: string }
                      aiConfidence: { type: number, minimum: 0, maximum: 1 }
      responses:
        '200':
          description: Combined initialization response
          headers:
            Cache-Control:
              schema: { type: string, example: 'public, s-maxage=30, stale-while-revalidate=120' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  config:
                    type: object
                    description: Site config (same shape as /sites/config response)
                    properties:
                      siteId: { type: string, format: uuid }
                      domain: { type: string }
                      plan: { type: string }
                      showWatermark: { type: boolean }
                      whiteLabel: { type: boolean }
                      isFrozen: { type: boolean }
                      contentCount: { type: integer }
                      contentLimit: { type: integer, nullable: true }
                      contentLimitReached: { type: boolean }
                      settings: { type: object }
                  registered:
                    type: array
                    description: Content registration results
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        hash: { type: string }
                        status: { type: string }
                        existing: { type: boolean }
                  statuses:
                    type: object
                    description: 'Hash → status map for all registered content'
                    additionalProperties: { type: string }
                  agents:
                    type: object
                    properties:
                      disclosures:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            agent_name: { type: string }
                            agent_type: { type: string }
                            disclosure_text: { type: string }
                      knownAgents:
                        type: array
                        items:
                          type: object
                          properties:
                            vendor: { type: string }
                            agent_type: { type: string }
                            script_patterns: { type: array, items: { type: string } }
                            default_disclosure_text: { type: string }
        '404':
          description: Site not found

  # ── Content ──────────────────────────────────────────────────────────────────
  /content/site/{siteId}:
    get:
      tags: [Content]
      summary: Get all content items for a site
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }

  /content/register:
    post:
      tags: [Content]
      summary: Register detected content items (called by snippet)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [siteKey, items]
              properties:
                siteKey: { type: string }
                items:
                  type: array
                  maxItems: 50
                  items:
                    type: object
                    properties:
                      hash: { type: string }
                      type: { type: string, enum: [text, image, video, audio] }
                      url: { type: string }
                      selector: { type: string }
                      preview: { type: string }
                      aiConfidence: { type: number }
      responses:
        '200':
          description: Registration result per item (may include limit_reached)

  /content/bulk-validate:
    put:
      tags: [Content]
      summary: Bulk validate up to 100 content items
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, status]
              properties:
                ids:
                  type: array
                  items: { type: string, format: uuid }
                  maxItems: 100
                status: { type: string, enum: [pending, ai, human] }

  /content/{id}/validate:
    put:
      tags: [Content]
      summary: Validate a single content item (Human-in-the-loop)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [pending, ai, human] }
                notes: { type: string }

  /content/{id}/exempt:
    put:
      tags: [Content]
      summary: Set Art. 50(5) exemption on content item
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [isExempt]
              properties:
                isExempt: { type: boolean }
                exemptReason: { type: string, enum: [artistic, satirical, fictional, editorial_control, obvious_ai, out_of_scope] }
                exemptNote: { type: string }

  /content/status/{siteKey}:
    get:
      tags: [Content]
      summary: Get content statuses by hash (used by snippet)
      security: []
      parameters:
        - name: siteKey
          in: path
          required: true
          schema: { type: string }
        - name: hashes
          in: query
          schema: { type: string, description: 'Comma-separated content hashes' }

  /content/onsite/validate:
    post:
      tags: [Content, Snippet]
      summary: Validate content from Visual On-Site Editor
      description: |
        Direct-save endpoint for the Visual Editor widget.
        Uses an editor JWT token (not the standard user JWT).
        Public endpoint — called from customer websites (any origin).

        Accepts a single item (`hash` at the top level) or a batch of up to
        200 items (`items[]`); one `status` applies to the whole call. This
        powers the editor's "mark rest of page as No-AI" bulk action.
        `status: exempt` sets the Art. 50(4) exemption (requires
        `exemptReason`) instead of a label; `ai`/`human` clear any exemption
        and the "changed" (needs_review) flag; `remove` deletes the items.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [ai, human, exempt, remove], description: 'Decision applied to all items in the call' }
                exemptReason: { type: string, enum: [artistic, satirical, fictional, editorial_control, obvious_ai, out_of_scope], description: 'Required when status is exempt' }
                hash: { type: string, description: 'Single-item mode' }
                items:
                  type: array
                  maxItems: 200
                  description: 'Bulk mode — each entry has hash/type/url/selector/preview'
                  items:
                    type: object
                    required: [hash]
                    properties:
                      hash: { type: string }
                      type: { type: string, enum: [text, image, video, audio] }
                      url: { type: string }
                      selector: { type: string }
                      preview: { type: string }
                type: { type: string, enum: [text, image, video, audio], description: 'Single-item mode' }
                url: { type: string, description: 'Single-item mode' }
                selector: { type: string, description: 'Single-item mode' }
                preview: { type: string, description: 'Single-item mode' }
      responses:
        '200':
          description: Content validated, exempted or removed
        '400':
          description: Invalid payload (bad status, missing exemptReason, or >200 items)
        '401':
          description: Missing or invalid editor token
        '403':
          description: Site no longer owned by the token user

  # ── Audit ─────────────────────────────────────────────────────────────────────
  /audit/site/{siteId}:
    get:
      tags: [Audit]
      summary: Get paginated audit logs for a site
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 500 }
        - name: action
          in: query
          schema: { type: string }

  /audit/all:
    get:
      tags: [Audit]
      summary: Get audit logs across all user sites

  /audit/export/{siteId}:
    get:
      tags: [Audit]
      summary: Export audit logs as JSON or CSV
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: format
          in: query
          schema: { type: string, enum: [json, csv], default: json }
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
      responses:
        '200':
          description: JSON export or CSV file download

  # ── Stripe ────────────────────────────────────────────────────────────────────
  /stripe/create-checkout:
    post:
      tags: [Stripe]
      summary: Create Stripe Checkout session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
                  enum: [starter, starter_yearly, pro_monthly, pro_yearly, agency, agency_yearly]
                successUrl: { type: string, format: uri }
                cancelUrl: { type: string, format: uri }
      responses:
        '200':
          description: Stripe Checkout URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

  /stripe/webhook:
    post:
      tags: [Stripe]
      summary: Stripe webhook (Stripe → Backend)
      security: []
      description: Stripe signature verified via STRIPE_WEBHOOK_SECRET

  /stripe/verify-session:
    post:
      tags: [Stripe]
      summary: Verify checkout session and activate plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sessionId]
              properties:
                sessionId: { type: string }

  /stripe/sync-my-subscription:
    post:
      tags: [Stripe]
      summary: Sync own subscription status from Stripe

  /stripe/portal:
    post:
      tags: [Stripe]
      summary: Create Stripe Customer Portal session
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

  # ── Scripts ───────────────────────────────────────────────────────────────────
  /scripts/report:
    post:
      tags: [Scripts]
      summary: Report a script finding from snippet (public)
      security: []

  /scripts/findings/{siteId}:
    get:
      tags: [Scripts]
      summary: Get script findings for a site
      parameters:
        - name: siteId
          in: path
          required: true
          schema: { type: string, format: uuid }

  /scripts/findings/{id}:
    put:
      tags: [Scripts]
      summary: Update / dismiss a script finding
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }

  /scripts/blocklist/{siteKey}:
    get:
      tags: [Scripts]
      summary: Get script blocklist for a site (used by snippet)
      security: []
      parameters:
        - name: siteKey
          in: path
          required: true
          schema: { type: string }

  # ── Agents ────────────────────────────────────────────────────────────────────
  /agents/config/{siteKey}:
    get:
      tags: [Agents]
      summary: Get agent disclosure config (public)
      security: []

  /agents/site/{siteId}:
    get:
      tags: [Agents]
      summary: List agent disclosures for a site
    post:
      tags: [Agents]
      summary: Create agent disclosure

  /agents/{id}:
    put:
      tags: [Agents]
      summary: Update agent disclosure
    delete:
      tags: [Agents]
      summary: Delete agent disclosure

  # ── Snippet ───────────────────────────────────────────────────────────────────
  # Diese drei liegen an der DOMAIN-WURZEL, nicht unter /api — deshalb der
  # servers-Override je Pfad. Sie standen bis 8.8.2026 als /snippet/... im
  # Dokument und lieferten damit ausnahmslos 404.
  /enforcer.js:
    servers:
      - url: https://aiactify-backend.vercel.app
        description: Production (Domain-Wurzel)
    get:
      tags: [Snippet]
      summary: Serve the client-side compliance enforcer script
      description: >
        Der Site Key wird NICHT als Query-Parameter uebergeben, sondern als
        data-site-id-Attribut am script-Tag gelesen:
        <script src="https://aiactify-backend.vercel.app/enforcer.js"
        data-site-id="a1b2c3d4e5f6a7b8"></script>
      security: []
      responses:
        '200':
          description: JavaScript file
          content:
            application/javascript:
              schema: { type: string }

  /pii/check:
    servers:
      - url: https://aiactify-backend.vercel.app
        description: Production (Domain-Wurzel)
    post:
      tags: [Snippet]
      summary: Check text for PII patterns
      security: []

  /pii/anonymize:
    servers:
      - url: https://aiactify-backend.vercel.app
        description: Production (Domain-Wurzel)
    post:
      tags: [Snippet]
      summary: Anonymize PII in text
      security: []
