# Sladd API v1 — OpenAPI 3.1
# Autoritativ kilde: docs/api-kontrakt.md (repo). Denne specen speiler kontrakten
# og deployet virkelighet: /v1/usage er planlagt men ikke aktiv ennå, og
# mode=deep returnerer 503 mode_unavailable inntil deep-gatewayen aktiveres.
openapi: 3.1.0
info:
  title: Sladd API
  version: "1"
  description: |
    Norsk PII-deteksjon og -redaksjon som HTTP-API. PDF eller tekst inn —
    funn-liste eller sladdet, verifisert dokument ut.

    **Flyktighetsløfte:** dokumenter behandles kun i minnet, skrives aldri til
    disk, og logges aldri. Usage-rader inneholder metadata (endepunkt, mode,
    sideantall) — aldri innhold, funn-tekst eller filnavn.

    **For AI-agenter:** Sladd finnes også som MCP-server — `npx @sladd-no/mcp`
    (lokal) eller `https://sladd.no/mcp` (Streamable HTTP med Bearer-header).
    Se https://sladd.no/api.
  contact:
    name: Sladd
    url: https://sladd.no/api
    email: post@sladd.no
servers:
  - url: https://sladd.no/api
security:
  - bearerAuth: []
paths:
  /v1/detect:
    post:
      operationId: detectPii
      summary: Detekter PII — funn-liste som JSON
      description: |
        Skann tekst eller PDF for norsk PII. Returnerer funn med tegn-offsets,
        type, confidence og anbefalt handling. Input på nøyaktig én av tre
        måter: multipart `file` (PDF), JSON `document_base64` (PDF) eller
        JSON `text` (ren tekst).
      requestBody:
        $ref: "#/components/requestBodies/DocumentInput"
      responses:
        "200":
          description: Funn-liste
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectResponse"
        "400": { $ref: "#/components/responses/InvalidDocument" }
        "401": { $ref: "#/components/responses/InvalidKey" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "422": { $ref: "#/components/responses/CouldNotParse" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ModeUnavailable" }
  /v1/redact:
    post:
      operationId: redactDocument
      summary: Sladd dokumentet — hovedproduktet
      description: |
        PDF inn → sladdet PDF ut (rasterisert: funn fjernes irreversibelt under
        sorte bokser) + rapportsammendrag i `X-Sladd-Report`-headeren (base64
        JSON, uten `detections[]`). Med `report=true` returneres
        `multipart/mixed` med PDF + full JSON-rapport.

        Tekst inn → JSON med `redacted_text` (style `label` eller `pseudonym`;
        PDF støtter kun `box` i v1).

        **Verification-blokken:** API-et re-skanner output-dokumentet. En
        redaksjon som lekker returneres aldri (500, ikke 200).

        `verification.passed` er `true` KUN når hvert detekterte funn er
        håndtert. Headless sladdes bare funn med `action: "auto"`; funn med
        `review`/`hint` blir stående i dokumentet. Da er `passed: false`,
        `stats.skippedReview > 0` og `verification.skipped[]` sier hvilke typer
        og posisjoner det gjelder — slik at du kan håndtere dem selv.

        **Sjekk alltid `skippedReview` før du behandler utdata som anonymisert.**
        `passed: true` betyr at alt som ble detektert er fjernet; det betyr
        fortsatt ikke at dokumentet er fritt for all PII, siden motoren kan ha
        oversett noe. For høysensitive dokumenter anbefales `mode=deep` når den
        er aktiv.
      requestBody:
        $ref: "#/components/requestBodies/RedactInput"
      responses:
        "200":
          description: >-
            Sladdet dokument. Stat-headerne settes på BEGGE svarformene — også
            tekst-svaret.
          headers:
            X-Sladd-Report:
              description: Base64-kodet JSON-sammendrag av rapporten (uten detections[]). Kun PDF-svar.
              schema: { type: string }
            X-Sladd-Detected-Total:
              description: Antall funn motoren detekterte.
              schema: { type: integer }
            X-Sladd-Redacted:
              description: Antall funn som faktisk ble sladdet.
              schema: { type: integer }
            X-Sladd-Skipped-Review:
              description: Detekterte funn som IKKE ble sladdet. Er denne > 0, er dokumentet ufullstendig sladdet.
              schema: { type: integer }
            X-Sladd-Redacted-Chars:
              description: Antall tegn som ble erstattet. Overlappende funn telles én gang.
              schema: { type: integer }
            X-Sladd-Redacted-Ratio:
              description: >-
                redactedChars / documentChars, 0-1. Gate mekanisk på denne for å oppdage
                et ubrukelig resultat uten å lese dokumentet — f.eks. over 0.4 ⇒ send til
                menneske. API-et sperrer ikke selv.
              schema: { type: number }
            X-Sladd-Policy:
              description: Hvilken redactPolicy svaret gjelder.
              schema: { $ref: "#/components/schemas/RedactPolicy" }
            X-Sladd-Pages:
              schema: { type: integer }
            X-Sladd-Verified:
              description: Speiler verification.passed.
              schema: { type: string, enum: ["true", "false"] }
            X-Sladd-Total:
              description: Utfaset alias for X-Sladd-Detected-Total.
              schema: { type: integer }
            X-Sladd-Auto-Redacted:
              description: Utfaset alias for X-Sladd-Redacted.
              schema: { type: integer }
          content:
            application/pdf:
              schema: { type: string, format: binary }
            multipart/mixed:
              schema:
                type: string
                format: binary
                description: To deler når report=true — `pdf` (application/pdf) og `report` (application/json, full rapport inkl. detections[]).
            application/json:
              schema:
                $ref: "#/components/schemas/RedactTextResponse"
        "400": { $ref: "#/components/responses/InvalidDocument" }
        "401": { $ref: "#/components/responses/InvalidKey" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "422": { $ref: "#/components/responses/CouldNotParse" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ModeUnavailable" }
  /v1/health:
    get:
      operationId: health
      summary: Status og versjon (uautentisert)
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, const: ok }
                  service: { type: string, const: sladd-api }
                  version: { type: string }
                  modes:
                    type: array
                    description: >-
                      Modi som faktisk kan besvares. En modus som står her gir ikke
                      mode_unavailable; en modus som mangler, gjør det. Utledes fra samme
                      kilde som endepunktene avviser fra — ingen miljøvariabel kan
                      annonsere en modus uten å gjøre den leverbar (BUG-008).
                    items: { type: string, enum: [fast, deep] }
                  pdf_standard_fonts:
                    type: boolean
                    description: >-
                      Om fontdata for ikke-innebygde base-14-fonter finnes i denne
                      deployen. False ⇒ PDF-er fra slike generatorer avvises med
                      could_not_parse i stedet for å gi en blank side (BUG-007).
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API-nøkkel `sk_sladd_…` som Bearer-token. Nøkler utstedes manuelt i v1 —
        kontakt post@sladd.no. Rate limit: 60 requests/minutt per nøkkel.
  requestBodies:
    DocumentInput:
      required: true
      content:
        multipart/form-data:
          schema:
            type: object
            required: [file]
            properties:
              file: { type: string, format: binary, description: PDF-fil }
              mode: { $ref: "#/components/schemas/Mode" }
              types: { type: string, description: "Kommaseparert typefilter, f.eks. PERSON_NAME,NATIONAL_ID" }
        application/json:
          schema:
            type: object
            description: Nøyaktig ett av `document_base64` og `text` skal være satt.
            properties:
              document_base64: { type: string, description: Base64-kodet PDF }
              text: { type: string, description: Ren tekst }
              mode: { $ref: "#/components/schemas/Mode" }
              types:
                type: array
                items: { $ref: "#/components/schemas/PIIType" }
    RedactInput:
      required: true
      content:
        multipart/form-data:
          schema:
            type: object
            required: [file]
            properties:
              file: { type: string, format: binary, description: PDF-fil }
              mode: { $ref: "#/components/schemas/Mode" }
              types: { type: string }
              style: { $ref: "#/components/schemas/Style" }
              filename_check: { type: string, enum: ["true", "false"] }
              redactPolicy: { $ref: "#/components/schemas/RedactPolicy" }
              report: { type: string, enum: ["true", "false"], description: true → multipart/mixed med full rapport }
        application/json:
          schema:
            type: object
            properties:
              document_base64: { type: string }
              text: { type: string }
              mode: { $ref: "#/components/schemas/Mode" }
              types:
                type: array
                items: { $ref: "#/components/schemas/PIIType" }
              style: { $ref: "#/components/schemas/Style" }
              filename_check: { type: boolean }
              redactPolicy: { $ref: "#/components/schemas/RedactPolicy" }
              report: { type: boolean }
  responses:
    InvalidDocument:
      description: Ugyldig/manglende dokument
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    InvalidKey:
      description: Manglende, ukjent eller tilbakekalt API-nøkkel
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    TooLarge:
      description: Over 20 MB
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    CouldNotParse:
      description: PDF kan ikke parses (korrupt, uten tekstlag, over 80 sider — meldingen sier sideantall, grense og at dokumentet kan deles opp)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    RateLimited:
      description: Over 60 requests/minutt for nøkkelen
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    ModeUnavailable:
      description: mode=deep er ikke aktivert ennå — bruk mode=fast
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
  schemas:
    Mode:
      type: string
      enum: [fast, deep]
      default: fast
      description: "fast: regex + ordliste (lag 1+2), deterministisk. deep: + NER-modell (lag 3), høyere recall, 3x fakturerbar enhet. deep er ikke aktiv ennå (503)."
    Style:
      type: string
      enum: [box, label, pseudonym]
      default: box
      description: "box: sort boks (eneste for PDF i v1). label: [Personnavn]. pseudonym: [PERSON_1]. label/pseudonym kun for text-input."
    PIIType:
      type: string
      enum:
        - PERSON_NAME
        - NATIONAL_ID
        - BANK_ACCOUNT
        - PHONE
        - EMAIL
        - ADDRESS
        - POSTAL_CODE
        - CASE_NUMBER
        - ORG_NUMBER
        - DATE_OF_BIRTH
        - DATE
        - LICENSE_PLATE
        - HEALTH_ID
        - HEALTH_INFO
        - DUF_NUMBER
        - OTHER
    ApiError:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Engelsk og stabil — kod mot denne.
          enum:
            - invalid_document
            - invalid_key
            - too_large
            - could_not_parse
            - rate_limited
            - mode_unavailable
            - internal_error
        message:
          type: string
          description: Norsk og menneskelesbar — kan endres uten varsel.
    Detection:
      type: object
      description: ScoredDetection fra @sladd/core — én kontrakt, ingen oversettelse.
      required: [start, end, text, type, confidence, sources, action]
      properties:
        start: { type: integer, description: 0-basert tegn-offset (inklusiv) }
        end: { type: integer, description: Tegn-offset (eksklusiv) }
        text: { type: string, description: Matchet tekst, verbatim }
        type: { $ref: "#/components/schemas/PIIType" }
        confidence: { type: number, minimum: 0, maximum: 0.99 }
        sources:
          type: array
          items: { type: string, enum: [regex, wordlist, heuristic, ner, manual] }
        action: { type: string, enum: [auto, review, hint] }
        bbox:
          type: object
          description: PDF-koordinater. Kun ved PDF-input.
          properties:
            x: { type: number }
            y: { type: number }
            width: { type: number }
            height: { type: number }
            page: { type: integer }
    DetectResponse:
      type: object
      required: [mode, pages, detections]
      properties:
        mode: { type: string }
        pages: { type: integer }
        detections:
          type: array
          items: { $ref: "#/components/schemas/Detection" }
    RedactPolicy:
      type: string
      default: auto
      pattern: "^(auto|all|threshold:(0(\.\d+)?|1(\.0+)?))$"
      description: >-
        Hvilke detekterte funn som faktisk sladdes. `auto` (default) tar kun funn med
        action=auto — uendret oppførsel. `all` tar alle detekterte funn. `threshold:<n>`
        tar alle funn med confidence >= n (n mellom 0 og 1), uavhengig av action.

        Defaulten endres ikke: `all` hever sladde-recall fra 60,4 % til 94,8 % på Sladds
        fem bransjefixturer, men andelen av dokumentet som forsvinner går fra 19,2 % til
        30,5 %, og mer på tabeller. Avveiningen er kallerens.

        `all` gir ikke full dekning — taket er det motoren detekterer (målt 94,8 %).

        Ugyldig verdi gir invalid_document (400); API-et faller aldri stilltiende tilbake
        til en annen policy.
      example: threshold:0.85
    SkippedFinding:
      type: object
      description: Et detektert funn som IKKE ble sladdet. Type og posisjon, aldri verdien.
      required: [type, action, confidence]
      properties:
        type: { $ref: "#/components/schemas/PIIType" }
        action: { type: string }
        confidence: { type: number }
        start: { type: integer, description: Tegn-offset i kildeteksten (kun tekst-input). }
        end: { type: integer }
        page: { type: integer, description: Sidetall (kun PDF-input). }
    Verification:
      type: object
      description: >-
        passed er true KUN når hvert detekterte funn er håndtert — sladdet eller
        eksplisitt avvist. Udekkede review-funn gir passed=false, og skipped[] sier
        hvilke. passed=true garanterer fortsatt IKKE at dokumentet er fritt for all
        PII: motoren kan ha oversett noe den aldri detekterte.
      required: [passed, policy, method, leaks, skipped]
      properties:
        passed: { type: boolean }
        policy:
          $ref: "#/components/schemas/RedactPolicy"
          description: >-
            Hvilken redactPolicy svaret gjelder. passed betyr noe annet under `all` enn
            under `auto`, så feltet står aldri alene: under `all` er skippedReview 0 per
            konstruksjon, og passed reduseres til «ingen lekkasje ved re-skann».
        method: { type: string, enum: ["re-scan (fast)", "re-scan (deep)"] }
        leaks:
          type: array
          description: Sladdet innhold som likevel ble funnet i output. Ikke-tom ⇒ 500, aldri 200.
          items: {}
        skipped:
          type: array
          description: Detekterte funn som ikke ble sladdet. Ikke-tom ⇒ passed=false.
          items: { $ref: "#/components/schemas/SkippedFinding" }
    RedactReport:
      type: object
      description: Inneholder ALDRI originaltekst — kun typer, pseudonymer, confidence, sidenummer.
      required: [version, mode, stats, verification]
      properties:
        version: { type: string }
        mode: { type: string }
        stats:
          type: object
          description: detectedTotal = redacted + skippedReview + rejected.
          required: [detectedTotal, redacted, skippedReview, rejected, pages]
          properties:
            detectedTotal: { type: integer, description: Antall funn motoren detekterte. }
            redacted: { type: integer, description: Antall funn som faktisk ble sladdet. }
            skippedReview: { type: integer, description: Detekterte funn som IKKE ble sladdet. }
            rejected: { type: integer, description: "Funn kalleren eksplisitt avviste (v1: alltid 0)." }
            pages: { type: integer }
            documentChars: { type: integer, description: Antall tegn i dokumentteksten — nevneren i redactedRatio. }
            redactedChars: { type: integer, description: Antall tegn erstattet. Overlappende funn telles én gang. }
            redactedRatio:
              type: number
              minimum: 0
              maximum: 1
              description: redactedChars / documentChars. Gate mekanisk på denne.
            total: { type: integer, deprecated: true, description: Alias for detectedTotal. }
            autoRedacted: { type: integer, deprecated: true, description: Alias for redacted. }
            reviewed: { type: integer, deprecated: true, description: Alias for skippedReview. }
        verification: { $ref: "#/components/schemas/Verification" }
        detections:
          type: array
          description: Kun i full rapport (report=true / text-input).
          items:
            type: object
            properties:
              type: { $ref: "#/components/schemas/PIIType" }
              redactedAs: { type: string }
              confidence: { type: number }
              action: { type: string }
              pageIndex: { type: integer }
    RedactTextResponse:
      type: object
      description: Svaret ved text-input.
      required: [mode, redacted_text, report]
      properties:
        mode: { type: string }
        redacted_text: { type: string }
        report: { $ref: "#/components/schemas/RedactReport" }
