openapi: 3.1.0

info:
  title: webaccess API
  version: "1.0.0"
  summary: Güvenli web erişimi ve site tarama altyapısı.
  description: |
    **webaccess**, "bir web adresine benim adıma güvenli biçimde git, gez, içeriğini
    çıkar ve yapılandırılmış olarak bana ver" işini yapan bağımsız bir servistir.
    İçerik hakkında yorum yapmaz, karar vermez, LLM çalıştırmaz - yalnızca güvenilir
    biçimde getirir.

    ## Kimlik doğrulama
    `/healthz` dışındaki her istek `X-API-Key` başlığı ister. Anahtarınız yalnızca
    hash olarak saklanır ve tek bir **tenant**'ı temsil eder.

    ```bash
    curl https://api.webaccess.searchestra.com/v1/crawl/latest?clientRef=marka-123 \
      -H "X-API-Key: wa_live_..."
    ```

    ## Çok-kiracılılık ve `clientRef`
    Her istekte kendi proje/marka kimliğinizi (`clientRef`) verirsiniz. Servis bunu
    doğrulanmış tenant'ınızla birleştirir; böylece **veri izolasyonu otomatiktir** -
    iki farklı müşteri aynı `clientRef`'i kullansa bile verileri asla karışmaz.

    ## Yanıt biçimi
    Başarılı yanıtlar bir `data` alanına sarılır. Hatalar `error` nesnesi döner:

    ```json
    { "error": { "code": "unauthorized", "message": "Geçersiz API anahtarı." } }
    ```

    ## Tarama modeli
    Tarama **asenkrondur**: `POST /v1/crawl/start` hemen bir `runId` döner, iş arka
    planda yürür. Sonucu webhook ile alır ya da tur durumunu poll edersiniz.

  contact:
    name: webaccess
    url: https://api.webaccess.searchestra.com

servers:
  - url: https://api.webaccess.searchestra.com
    description: Production
  - url: http://localhost:8090
    description: Yerel geliştirme

security:
  - apiKey: []

tags:
  - name: Tarama
    description: Çok sayfalı asenkron site tarama ve taranan içeriğe erişim.
  - name: İndeksleme
    description: Taranan sayfaları arama/RAG için parçalara bölme ve embedding üretme.
  - name: Ad-hoc araçlar
    description: Tek bir URL/alan adı için anlık çekme, performans ve robots/sitemap.
  - name: Sistem
    description: Sağlık kontrolü.

paths:
  /healthz:
    get:
      tags: [Sistem]
      operationId: healthCheck
      summary: Sağlık kontrolü
      description: Servisin ayakta olduğunu doğrular. Kimlik doğrulama gerektirmez.
      security: []
      responses:
        "200":
          description: Servis sağlıklı.

  /v1/crawl/start:
    post:
      tags: [Tarama]
      operationId: startCrawl
      summary: Tarama başlat
      description: |
        Bir alan adı için asenkron taramayı başlatır ve hemen bir `runId` döner.
        İşin kendisi arka planda yürür - durumu `GET /v1/crawl/{runId}` ile izleyin
        veya webhook bildirimini bekleyin.

        Aynı proje için aynı alan adı yakın zamanda tarandıysa (varsayılan 24 saat),
        servis yeni bir tarama yapmak yerine son turu döndürür. `force: true` bunu
        atlar ve sıfırdan yeniden tarar.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StartCrawlRequest" }
            examples:
              domain:
                summary: Tüm alan adını tara
                value: { domain: "godiva.com.tr", scope: "domain", clientRef: "marka-123" }
              forceReindex:
                summary: Tazelik kısayolunu atlayarak yeniden tara
                value: { domain: "godiva.com.tr", scope: "domain", clientRef: "marka-123", force: true, maxPages: 50 }
      responses:
        "201":
          description: Tarama kuyruğa alındı.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CrawlRunRef" }
              example:
                data: { runId: "01a0347a-3cd9-70e1-9dce-c2dd5962a4e6", status: "pending" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /v1/crawl/latest:
    get:
      tags: [Tarama]
      operationId: getLatestCrawl
      summary: En son turu getir
      description: |
        Projenin en son tarama turunu döner - `runId`'yi bilmeden "şu an aktif bir
        tarama var mı, en son ne tarandı?" sorusunun cevabı. Hiç tur yoksa boş bir
        `data` nesnesi (`{}`) döner.
      parameters:
        - $ref: "#/components/parameters/ClientRefQuery"
      responses:
        "200":
          description: En son tur (yoksa boş nesne).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CrawlRun" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/crawl/{runId}:
    get:
      tags: [Tarama]
      operationId: getCrawlRun
      summary: Tur durumunu getir
      description: Belirli bir tarama turunun anlık durumunu ve ilerleme sayaçlarını döner. İstemci bu ucu `done`/`failed` olana dek poll eder.
      parameters:
        - name: runId
          in: path
          required: true
          description: "`POST /v1/crawl/start` ile dönen tur kimliği."
          schema: { type: string }
        - $ref: "#/components/parameters/ClientRefQuery"
      responses:
        "200":
          description: Tur durumu.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CrawlRun" }
              example:
                data:
                  runId: "01a0347a-3cd9-70e1-9dce-c2dd5962a4e6"
                  status: "running"
                  domain: "godiva.com.tr"
                  scope: "domain"
                  pagesFound: 90
                  pagesCrawled: 74
                  pagesFailed: 2
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/crawl/pages:
    get:
      tags: [Tarama]
      operationId: listPages
      summary: Taranan sayfaları listele
      description: |
        Projenin taranmış sayfalarını çıkarılmış içerikleriyle döner (başlık,
        açıklama, görünür metin, durum kodu, kanonik URL, yapısal veri).

        Varsayılan olarak ham HTML de dahildir. İçeriği analiz edip HTML'e ihtiyaç
        duymuyorsanız `summary=true` ile daha hafif bir yanıt alın.
      parameters:
        - $ref: "#/components/parameters/ClientRefQuery"
        - name: domain
          in: query
          description: Yalnızca bu alan adına ait sayfaları döndür (opsiyonel).
          schema: { type: string }
        - name: limit
          in: query
          description: Döndürülecek maksimum sayfa sayısı.
          schema: { type: integer, default: 100 }
        - name: summary
          in: query
          description: "`true` ise yanıt ham HTML içermez (daha hafif)."
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Taranan sayfalar.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Page" }
              example:
                data:
                  - url: "https://godiva.com.tr/urunler"
                    canonicalUrl: "https://godiva.com.tr/urunler"
                    title: "Ürünler"
                    description: "Godiva çikolata koleksiyonu"
                    textExcerpt: "El yapımı Belçika çikolataları…"
                    statusCode: 200
                    fetchedVia: "http"
                    depth: 1
                    crawledAt: "2026-08-24T10:12:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/crawl/index:
    post:
      tags: [İndeksleme]
      operationId: indexProject
      summary: Projeyi indeksle
      description: |
        Projenin tüm taranmış sayfalarını arama/RAG için paragraf sınırlarında
        parçalara böler ve - bir embedding sağlayıcı yapılandırılmışsa - vektör üretir.
        Embedding yoksa parçalar yine kaydedilir; yalnızca klasik tam-metin arama çalışır.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IndexRequest" }
            example: { clientRef: "marka-123" }
      responses:
        "200":
          description: İndeksleme sonucu.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/IndexResult" }
              example:
                data: { pagesIndexed: 74, chunksCreated: 210, chunksEmbedded: 210 }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/fetch:
    post:
      tags: [Ad-hoc araçlar]
      operationId: fetchUrl
      summary: Tek URL çek
      description: |
        Tek bir adresi ham HTML, tüm HTTP başlıkları ve tam yönlendirme zinciriyle
        birlikte getirir. Zamanlama metrikleri (TTFB, toplam süre) ve gövde boyutu dahil.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FetchRequest" }
            example: { url: "https://competitora.com" }
      responses:
        "200":
          description: Çekilen içerik.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FetchResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /v1/cwv:
    post:
      tags: [Ad-hoc araçlar]
      operationId: measureCwv
      summary: Core Web Vitals ölç
      description: |
        Gerçek bir headless tarayıcı açıp bir sayfanın performans metriklerini
        ölçer: LCP (en büyük içeriğin yüklenmesi), CLS (görsel kayma), TBT
        (toplam bloklama süresi).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UrlRequest" }
            example: { url: "https://competitora.com" }
      responses:
        "200":
          description: Ölçüm sonuçları.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CwvResult" }
              example:
                data: { lcp: 2100, cls: 0.04, tbt: 150, ttfb: 180, lcpSupported: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /v1/robots:
    post:
      tags: [Ad-hoc araçlar]
      operationId: getRobots
      summary: robots.txt getir
      description: Bir alan adının robots.txt kurallarını ham metin olarak döner.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DomainRequest" }
            example: { domain: "competitora.com" }
      responses:
        "200":
          description: robots.txt içeriği.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/RobotsResult" }
              example:
                data: { found: true, rawBody: "User-agent: *\nDisallow: /admin" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sitemap:
    post:
      tags: [Ad-hoc araçlar]
      operationId: getSitemap
      summary: Sitemap adreslerini getir
      description: |
        Bir alan adının site haritasındaki adresleri listeler. Index sitemap ve
        sıkıştırılmış (`.xml.gz`) sitemap'ler desteklenir. Bilinen sitemap yollarını
        `hints` ile verebilirsiniz.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SitemapRequest" }
            example: { domain: "competitora.com", hints: [] }
      responses:
        "200":
          description: Sitemap adresleri.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SitemapResult" }
              example:
                data: { urls: ["https://competitora.com/urun-1", "https://competitora.com/urun-2"] }
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Tenant'ınıza ait API anahtarı. `/healthz` dışındaki her istekte zorunludur.

  parameters:
    ClientRefQuery:
      name: clientRef
      in: query
      required: true
      description: Kendi proje/marka kimliğiniz. Veri izolasyonu için tenant'ınızla birleştirilir.
      schema: { type: string, example: "marka-123" }

  responses:
    Unauthorized:
      description: API anahtarı eksik veya geçersiz.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: "unauthorized", message: "Geçersiz API anahtarı." }
    NotFound:
      description: Kaynak bulunamadı.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: "not_found", message: "Tur bulunamadı." }
    ValidationError:
      description: İstek gövdesi geçersiz.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: "validation_failed", message: "Geçersiz alan adı." }

  schemas:
    Scope:
      type: string
      description: |
        Taramanın kapsamı:
        * `domain` - tüm alt alan adları dahil (varsayılan)
        * `subdomain` - yalnızca birebir aynı host
        * `path` - verilen dizin öneki altındaki yollar
        * `exact_url` - yalnızca verilen tek sayfa
      enum: [domain, subdomain, path, exact_url]
      default: domain

    CrawlStatus:
      type: string
      enum: [pending, running, done, failed]

    StartCrawlRequest:
      type: object
      required: [domain, clientRef]
      properties:
        domain:
          type: string
          description: Taranacak alan adı.
          example: "godiva.com.tr"
        clientRef:
          type: string
          description: Kendi proje/marka kimliğiniz.
          example: "marka-123"
        scope: { $ref: "#/components/schemas/Scope" }
        force:
          type: boolean
          description: "`true` ise tazelik kısayolunu atlar ve sıfırdan yeniden tarar."
          default: false
        maxPages:
          type: integer
          description: Bu tura özgü sayfa bütçesi (opsiyonel). Gönderilmezse servis varsayılanı kullanılır.

    CrawlRunRef:
      type: object
      properties:
        runId: { type: string }
        status: { $ref: "#/components/schemas/CrawlStatus" }

    CrawlRun:
      type: object
      description: Bir tarama turunun durumu ve ilerleme sayaçları.
      properties:
        runId: { type: string }
        status: { $ref: "#/components/schemas/CrawlStatus" }
        domain: { type: string }
        scope: { $ref: "#/components/schemas/Scope" }
        pagesFound:
          type: integer
          description: Keşfedilen toplam sayfa sayısı.
        pagesCrawled:
          type: integer
          description: Başarıyla çekilen sayfa sayısı.
        pagesFailed:
          type: integer
          description: Çekilemeyen sayfa sayısı.
        error:
          type: string
          description: Yalnızca `status = failed` olduğunda döner.

    Page:
      type: object
      properties:
        url: { type: string }
        canonicalUrl: { type: string }
        title: { type: string }
        description: { type: string }
        textExcerpt:
          type: string
          description: Sayfanın görünür metninden çıkarılmış özet.
        statusCode: { type: integer }
        contentHash:
          type: string
          description: Temizlenmiş metnin özeti - yinelenen içeriği tespit için.
        fetchedVia:
          type: string
          description: Sayfanın nasıl çekildiği.
          enum: [http, browser]
        depth:
          type: integer
          description: Başlangıç adresinden link derinliği.
        meta:
          type: object
          description: Çıkarılan meta etiketleri.
          additionalProperties: true
        jsonLd:
          description: Sayfadaki JSON-LD yapısal verisi (varsa).
        rawHtml:
          type: string
          description: Ham HTML gövdesi. Yalnızca `summary=true` verilmediğinde döner.
        crawledAt: { type: string, format: date-time }

    IndexRequest:
      type: object
      required: [clientRef]
      properties:
        clientRef: { type: string, example: "marka-123" }

    IndexResult:
      type: object
      properties:
        pagesIndexed: { type: integer }
        chunksCreated: { type: integer }
        chunksEmbedded:
          type: integer
          description: Embedding üretilen parça sayısı (sağlayıcı yoksa 0).

    FetchRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, example: "https://competitora.com" }
        maxAge:
          type: string
          description: "Önbellek penceresini daraltır (Go süre biçimi, ör. `10m`). Yalnızca paylaşımlı önbellek açıkken etkilidir."
        force:
          type: boolean
          description: "`true` ise önbelleği atlar, taze sonuç alır."
          default: false

    FetchResult:
      type: object
      properties:
        html: { type: string }
        headers:
          type: object
          description: HTTP yanıt başlıkları (her biri değer dizisi).
          additionalProperties:
            type: array
            items: { type: string }
        statusCode: { type: integer }
        finalUrl:
          type: string
          description: Yönlendirmeler sonrası nihai adres.
        redirectChain:
          type: array
          items: { $ref: "#/components/schemas/RedirectHop" }
        ttfbMs:
          type: integer
          description: İlk bayta kadar geçen süre (ms).
        totalMs:
          type: integer
          description: Toplam süre (ms).
        bodyBytes: { type: integer }
        fetchedVia:
          type: string
          enum: [http, browser]
        robotsBlocked:
          type: boolean
          description: robots.txt bu adresi engelliyorsa `true`.
        fromCache:
          type: boolean
          description: Sonuç paylaşımlı önbellekten döndüyse `true`.

    RedirectHop:
      type: object
      properties:
        url: { type: string }
        status: { type: integer }

    UrlRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, example: "https://competitora.com" }

    CwvResult:
      type: object
      properties:
        lcp:
          type: number
          description: Largest Contentful Paint (ms).
        cls:
          type: number
          description: Cumulative Layout Shift (birimsiz).
        tbt:
          type: number
          description: Total Blocking Time (ms).
        ttfb:
          type: number
          description: Time To First Byte (ms).
        loadEnd:
          type: number
          description: Yükleme tamamlanma süresi (ms).
        longTasks:
          type: integer
          description: Gözlemlenen uzun görev sayısı.
        lcpSupported: { type: boolean }
        clsSupported: { type: boolean }
        tbtSupported: { type: boolean }
        navObserved: { type: boolean }

    DomainRequest:
      type: object
      required: [domain]
      properties:
        domain: { type: string, example: "competitora.com" }

    RobotsResult:
      type: object
      properties:
        found:
          type: boolean
          description: robots.txt bulunduysa `true`.
        rawBody:
          type: string
          description: robots.txt ham içeriği.

    SitemapRequest:
      type: object
      required: [domain]
      properties:
        domain: { type: string, example: "competitora.com" }
        hints:
          type: array
          description: Bilinen sitemap yolları (opsiyonel).
          items: { type: string }

    SitemapResult:
      type: object
      properties:
        urls:
          type: array
          items: { type: string }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [validation_failed, unauthorized, forbidden, not_found, conflict, duplicate, quota_exceeded, rate_limited, unavailable, internal]
            message: { type: string }
            details:
              description: Hataya özel ek bağlam (opsiyonel).
