components:
  schemas:
    APIKeyCreateIn:
      description: >-
        Request body for creating a new API key.


        The CHECK constraint on the DB enforces scope coherence; we mirror it in

        Pydantic for fast client-side feedback. The router pre-validates that
        the

        actor can actually issue at the requested scope (super_admin → org,

        team_admin → team, team member → project).
      properties:
        expires_in_days:
          anyOf:
            - maximum: 1825
              minimum: 1
              type: integer
            - type: 'null'
          description: >-
            Optional TTL in days. The key stops authenticating after this many
            days. Omit for a non-expiring key (CI keys should set one and
            rotate). Max 1825 (5 years).
          title: Expires In Days
        name:
          maxLength: 100
          minLength: 1
          title: Name
          type: string
        project_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Project Id
        scope:
          enum:
            - org
            - team
            - project
          title: Scope
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Team Id
      required:
        - name
        - scope
      title: APIKeyCreateIn
      type: object
    APIKeyCreateOut:
      description: |-
        Response from POST /v1/api-keys.

        ``raw_key`` is the only place the plaintext is ever surfaced. The client
        is responsible for storing it (e.g. as a CI secret); a subsequent GET on
        the key returns the metadata-only :class:`APIKeyListItem` shape.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        expires_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Expires At
        id:
          format: uuid
          title: Id
          type: string
        key_prefix:
          title: Key Prefix
          type: string
        name:
          title: Name
          type: string
        project_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Project Id
        raw_key:
          description: >-
            The plaintext bearer key (format: tos_<prefix>_<secret>). Returned
            exactly once at issuance; capture it client-side. Subsequent reads
            only return metadata.
          title: Raw Key
          type: string
        scope:
          enum:
            - org
            - team
            - project
          title: Scope
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Team Id
      required:
        - id
        - key_prefix
        - name
        - scope
        - team_id
        - project_id
        - created_by_user_id
        - created_at
        - raw_key
      title: APIKeyCreateOut
      type: object
    APIKeyListItem:
      description: List-row shape — never includes the plaintext or the hash.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_email:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Email of the issuing user, so the management UI can show a
            human-readable creator column. None when the issuer account was
            deleted (created_by_user_id was SET NULL) or the user row is
            otherwise gone. PII note: this list is only reachable through the
            key-governance visibility boundary (issuer / team members /
            super_admin), so the email is not exposed beyond actors who can
            already manage the key.
          title: Created By Email
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        expires_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Expires At
        id:
          format: uuid
          title: Id
          type: string
        key_prefix:
          title: Key Prefix
          type: string
        last_used_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Last Used At
        name:
          title: Name
          type: string
        project_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Project Id
        revoked_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Revoked At
        scope:
          enum:
            - org
            - team
            - project
          title: Scope
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Team Id
      required:
        - id
        - key_prefix
        - name
        - scope
        - team_id
        - project_id
        - created_by_user_id
        - created_at
        - last_used_at
        - revoked_at
      title: APIKeyListItem
      type: object
    APIKeyListPage:
      description: Paginated list of API keys.
      properties:
        items:
          items:
            $ref: '#/components/schemas/APIKeyListItem'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: APIKeyListPage
      type: object
    AdminDiskItem:
      description: >-
        One mount / volume entry returned by ``GET /v1/admin/disk``.


        ``used_pct`` is computed server-side as ``used_bytes / total_bytes *
        100``

        rounded to one decimal so the UI does not need to repeat the math.

        Threshold cells are static configuration (80% warn, 90% critical) for

        Phase 4; future PRs may make them per-mount tunable.
      properties:
        error:
          anyOf:
            - type: string
            - type: 'null'
          description: Set when telemetry could not be collected (e.g. mount missing).
          title: Error
        free_bytes:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          title: Free Bytes
        name:
          enum:
            - workspace
            - trivy_db
            - postgres
            - redis
          title: Name
          type: string
        path:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Filesystem path the bytes were read from (only set for the
            filesystem-backed ``workspace`` / ``trivy_db`` entries — DB-backed
            entries have no single canonical path).
          title: Path
        status:
          description: >-
            ok / degraded / down derived from ``used_pct`` against the
            thresholds. ``degraded`` = warning band, ``down`` = critical.
          enum:
            - ok
            - degraded
            - down
          title: Status
          type: string
        threshold_critical:
          default: 90
          title: Threshold Critical
          type: number
        threshold_warning:
          default: 80
          title: Threshold Warning
          type: number
        total_bytes:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          title: Total Bytes
        used_bytes:
          minimum: 0
          title: Used Bytes
          type: integer
        used_pct:
          anyOf:
            - maximum: 100
              minimum: 0
              type: number
            - type: 'null'
          title: Used Pct
      required:
        - name
        - used_bytes
        - status
      title: AdminDiskItem
      type: object
    AdminDiskOut:
      description: Response of ``GET /v1/admin/disk``.
      properties:
        collected_at:
          format: date-time
          title: Collected At
          type: string
        items:
          items:
            $ref: '#/components/schemas/AdminDiskItem'
          title: Items
          type: array
      required:
        - items
        - collected_at
      title: AdminDiskOut
      type: object
    AdminScanListItem:
      description: One row in the admin scan queue listing — joins scan + project + team.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        duration_seconds:
          anyOf:
            - type: number
            - type: 'null'
          title: Duration Seconds
        error_message:
          anyOf:
            - type: string
            - type: 'null'
          title: Error Message
        finished_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            Mapped from ``scans.completed_at``. Renamed for clarity in the admin
            queue UI.
          title: Finished At
        id:
          format: uuid
          title: Id
          type: string
        kind:
          title: Kind
          type: string
        progress_percent:
          default: 0
          title: Progress Percent
          type: integer
        project_id:
          format: uuid
          title: Project Id
          type: string
        project_name:
          title: Project Name
          type: string
        requested_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Requested By User Id
        started_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Started At
        status:
          enum:
            - queued
            - running
            - succeeded
            - failed
            - cancelled
          title: Status
          type: string
        team_id:
          format: uuid
          title: Team Id
          type: string
        team_name:
          title: Team Name
          type: string
      required:
        - id
        - project_id
        - project_name
        - team_id
        - team_name
        - status
        - kind
        - created_at
      title: AdminScanListItem
      type: object
    AdminScanListPage:
      description: Paginated response of ``GET /v1/admin/scans``.
      properties:
        items:
          items:
            $ref: '#/components/schemas/AdminScanListItem'
          title: Items
          type: array
        page:
          minimum: 1
          title: Page
          type: integer
        page_size:
          minimum: 1
          title: Page Size
          type: integer
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: AdminScanListPage
      type: object
    AdminTeamCreate:
      description: Body for ``POST /v1/admin/teams``.
      properties:
        description:
          anyOf:
            - maxLength: 1024
              type: string
            - type: 'null'
          title: Description
        name:
          maxLength: 255
          minLength: 1
          title: Name
          type: string
        slug:
          maxLength: 64
          minLength: 1
          title: Slug
          type: string
      required:
        - name
        - slug
      title: AdminTeamCreate
      type: object
    AdminTeamDetail:
      description: Full detail view used by the right-side drawer in the Teams admin.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        description:
          anyOf:
            - type: string
            - type: 'null'
          title: Description
        id:
          format: uuid
          title: Id
          type: string
        members:
          items:
            $ref: '#/components/schemas/AdminTeamMember'
          title: Members
          type: array
        name:
          title: Name
          type: string
        project_count:
          default: 0
          title: Project Count
          type: integer
        slug:
          title: Slug
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - name
        - slug
        - created_at
        - updated_at
      title: AdminTeamDetail
      type: object
    AdminTeamListItem:
      description: Row in the paginated team list — includes counts for the admin table.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        description:
          anyOf:
            - type: string
            - type: 'null'
          title: Description
        id:
          format: uuid
          title: Id
          type: string
        member_count:
          default: 0
          title: Member Count
          type: integer
        name:
          title: Name
          type: string
        project_count:
          default: 0
          title: Project Count
          type: integer
        slug:
          title: Slug
          type: string
      required:
        - id
        - name
        - slug
        - created_at
      title: AdminTeamListItem
      type: object
    AdminTeamListPage:
      properties:
        items:
          items:
            $ref: '#/components/schemas/AdminTeamListItem'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: AdminTeamListPage
      type: object
    AdminTeamMember:
      description: Embedded member row in the team detail response.
      properties:
        email:
          title: Email
          type: string
        full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Full Name
        role:
          title: Role
          type: string
        user_id:
          format: uuid
          title: User Id
          type: string
      required:
        - user_id
        - email
        - role
      title: AdminTeamMember
      type: object
    AdminTeamMemberAdd:
      description: Body for ``POST /v1/admin/teams/{id}/members``.
      properties:
        role:
          description: Either team_admin or developer.
          title: Role
          type: string
        user_id:
          format: uuid
          title: User Id
          type: string
      required:
        - user_id
        - role
      title: AdminTeamMemberAdd
      type: object
    AdminTeamUpdate:
      description: Body for ``PATCH /v1/admin/teams/{id}``.
      properties:
        description:
          anyOf:
            - maxLength: 1024
              type: string
            - type: 'null'
          title: Description
        name:
          anyOf:
            - maxLength: 255
              minLength: 1
              type: string
            - type: 'null'
          title: Name
        slug:
          anyOf:
            - maxLength: 64
              minLength: 1
              type: string
            - type: 'null'
          title: Slug
      title: AdminTeamUpdate
      type: object
    AdminUserDetail:
      description: Full detail view used by the right-side drawer in the Users admin.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        email:
          title: Email
          type: string
        full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Full Name
        id:
          format: uuid
          title: Id
          type: string
        is_active:
          title: Is Active
          type: boolean
        is_superuser:
          title: Is Superuser
          type: boolean
        last_login_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Last Login At
        memberships:
          items:
            $ref: '#/components/schemas/TeamMembershipPublic'
          title: Memberships
          type: array
        scan_count:
          default: 0
          title: Scan Count
          type: integer
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - email
        - is_active
        - is_superuser
        - created_at
        - updated_at
      title: AdminUserDetail
      type: object
    AdminUserListItem:
      description: >-
        Row in the paginated list response (lightweight).


        H-2: ``role`` / ``team_count`` are a membership *rollup*
        (highest-effective

        role + membership count) computed by ``list_users`` in one aggregate
        query,

        so the role column / team count no longer require opening the detail

        drawer. Full memberships stay on :class:`AdminUserDetail`.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        email:
          title: Email
          type: string
        full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Full Name
        id:
          format: uuid
          title: Id
          type: string
        is_active:
          title: Is Active
          type: boolean
        is_superuser:
          title: Is Superuser
          type: boolean
        last_login_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Last Login At
        role:
          default: developer
          enum:
            - super_admin
            - team_admin
            - developer
          title: Role
          type: string
        team_count:
          default: 0
          title: Team Count
          type: integer
      required:
        - id
        - email
        - is_active
        - is_superuser
        - created_at
      title: AdminUserListItem
      type: object
    AdminUserListPage:
      description: Paginated list envelope.
      properties:
        items:
          items:
            $ref: '#/components/schemas/AdminUserListItem'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: AdminUserListPage
      type: object
    AdminUserRoleUpdate:
      description: Body for ``PATCH /v1/admin/users/{id}/role``.
      properties:
        role:
          description: One of super_admin / team_admin / developer.
          title: Role
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          description: >-
            Required when role is team_admin or developer; ignored for
            super_admin.
          title: Team Id
      required:
        - role
      title: AdminUserRoleUpdate
      type: object
    AffectedComponent:
      description: A component_version affected by the same CVE in the same scan.
      properties:
        component_version_id:
          format: uuid
          title: Component Version Id
          type: string
        fixed_version:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Version that remediates this CVE for this component, when the scan
            pipeline could determine one from DT findings (v2.2). Per-(component
            × CVE): the same CVE may be fixed at different versions across
            packages. ``null`` when DT reported no fix version, or for findings
            scanned before v2.2.
          title: Fixed Version
        name:
          title: Name
          type: string
        purl:
          anyOf:
            - type: string
            - type: 'null'
          title: Purl
        version:
          title: Version
          type: string
      required:
        - component_version_id
        - name
        - version
      title: AffectedComponent
      type: object
    AffectedComponentByLicense:
      description: A component_version that carries the license shown in the drawer.
      properties:
        component_name:
          title: Component Name
          type: string
        component_version_id:
          format: uuid
          title: Component Version Id
          type: string
        kind:
          enum:
            - declared
            - concluded
            - detected
          title: Kind
          type: string
        source_path:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Repo-relative path of the file that produced the finding (e.g.
            LICENSE, package.json). Null when ORT did not record one.
          title: Source Path
        version:
          title: Version
          type: string
      required:
        - component_version_id
        - component_name
        - version
        - kind
      title: AffectedComponentByLicense
      type: object
    AffectedComponentByObligation:
      description: |-
        A component_version in the latest scan that carries the parent license.

        Mirrors :class:`schemas.license_detail.AffectedComponentByLicense` but
        keyed via the obligation's parent license rather than a license_finding
        row directly.
      properties:
        component_name:
          title: Component Name
          type: string
        component_version_id:
          format: uuid
          title: Component Version Id
          type: string
        version:
          title: Version
          type: string
      required:
        - component_version_id
        - component_name
        - version
      title: AffectedComponentByObligation
      type: object
    ApprovalCreateIn:
      description: Request body for creating a new approval request.
      properties:
        component_id:
          format: uuid
          title: Component Id
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
      required:
        - component_id
        - project_id
      title: ApprovalCreateIn
      type: object
    ApprovalListPage:
      description: Paginated list of approval rows.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ApprovalOut'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: ApprovalListPage
      type: object
    ApprovalOut:
      description: Single component-approval row, safe to serialise to the caller.
      properties:
        component_id:
          format: uuid
          title: Component Id
          type: string
        component_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Component Name
        component_purl:
          anyOf:
            - type: string
            - type: 'null'
          title: Component Purl
        decided_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Decided At
        decided_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Decided By User Id
        decision_note:
          anyOf:
            - type: string
            - type: 'null'
          title: Decision Note
        id:
          format: uuid
          title: Id
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
        project_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Project Name
        project_slug:
          anyOf:
            - type: string
            - type: 'null'
          title: Project Slug
        requested_at:
          format: date-time
          title: Requested At
          type: string
        requested_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Requested By User Id
        status:
          $ref: '#/components/schemas/ApprovalStatus'
        team_id:
          format: uuid
          title: Team Id
          type: string
        version:
          title: Version
          type: integer
      required:
        - id
        - component_id
        - project_id
        - team_id
        - requested_by_user_id
        - requested_at
        - status
        - decided_by_user_id
        - decided_at
        - decision_note
        - version
      title: ApprovalOut
      type: object
    ApprovalStatus:
      description: |-
        Python mirror of the ``approval_status`` Postgres ENUM.

        Using ``str`` as the mixin lets Pydantic / JSON serialisation treat the
        value as a plain string without extra coercion.
      enum:
        - pending
        - under_review
        - approved
        - rejected
      title: ApprovalStatus
      type: string
    ApprovalTransitionIn:
      description: >-
        Request body for transitioning an approval's status.


        ``action`` is limited to the three states a human reviewer can drive.

        The server assigns ``pending`` on create; it is never a valid action
        here.

        ``decision_note`` is optional for ``under_review`` but strongly
        encouraged

        for ``rejected`` (the UI should warn, not block).
      properties:
        action:
          enum:
            - under_review
            - approved
            - rejected
          title: Action
          type: string
        decision_note:
          anyOf:
            - maxLength: 2000
              type: string
            - type: 'null'
          title: Decision Note
      required:
        - action
      title: ApprovalTransitionIn
      type: object
    AuditLogItem:
      description: One row in the admin audit log search response.
      properties:
        action:
          title: Action
          type: string
        actor_email:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Joined from ``users.email`` for display. ``null`` when the actor was
            deleted (FK is ``ondelete='SET NULL'``) or the row was
            system-initiated.
          title: Actor Email
        actor_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Actor User Id
        created_at:
          format: date-time
          title: Created At
          type: string
        diff:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          title: Diff
        id:
          format: uuid
          title: Id
          type: string
        request_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Id
        target_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Target Id
        target_table:
          title: Target Table
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Team Id
      required:
        - id
        - created_at
        - target_table
        - action
      title: AuditLogItem
      type: object
    AuditLogListPage:
      description: Paginated response of ``GET /v1/admin/audit``.
      properties:
        has_more:
          title: Has More
          type: boolean
        items:
          items:
            $ref: '#/components/schemas/AuditLogItem'
          title: Items
          type: array
        page:
          minimum: 1
          title: Page
          type: integer
        page_size:
          minimum: 1
          title: Page Size
          type: integer
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
        - has_more
      title: AuditLogListPage
      type: object
    BackupInfo:
      description: One row in the admin backup listing.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        db_revision:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Alembic head recorded in the manifest at backup time. ``None`` when
            the manifest was missing or unreadable (rare — emitted only by the
            script's ``unknown`` fallback).
          title: Db Revision
        kind:
          enum:
            - auto
            - manual
          title: Kind
          type: string
        name:
          description: >-
            Backup directory name. Format: ``{kind}-YYYYMMDDTHHMMSSZ``. Used as
            the path component for download / delete / restore.
          title: Name
          type: string
        size_bytes:
          minimum: 0
          title: Size Bytes
          type: integer
      required:
        - name
        - kind
        - created_at
        - size_bytes
      title: BackupInfo
      type: object
    BackupListResponse:
      description: Response of ``GET /v1/admin/backup``.
      properties:
        items:
          items:
            $ref: '#/components/schemas/BackupInfo'
          title: Items
          type: array
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - total
      title: BackupListResponse
      type: object
    BackupRestoreResponse:
      description: Response of ``POST /v1/admin/backup/restore`` — Celery enqueue receipt.
      properties:
        message:
          title: Message
          type: string
        task_id:
          title: Task Id
          type: string
      required:
        - task_id
        - message
      title: BackupRestoreResponse
      type: object
    BackupTriggerResponse:
      description: Response of ``POST /v1/admin/backup`` — Celery enqueue receipt.
      properties:
        name:
          description: >-
            Pre-computed backup directory name the task will create. Useful for
            the UI to poll the listing without waiting for task completion.
          title: Name
          type: string
        task_id:
          title: Task Id
          type: string
      required:
        - task_id
        - name
      title: BackupTriggerResponse
      type: object
    Body_import_project_vex_endpoint_v1_projects__project_id__vex_import_post:
      properties:
        upload:
          contentMediaType: application/octet-stream
          description: An OpenVEX or CycloneDX VEX JSON document (format auto-detected).
          title: Upload
          type: string
      required:
        - upload
      title: >-
        Body_import_project_vex_endpoint_v1_projects__project_id__vex_import_post
      type: object
    Body_restore_backup_endpoint_v1_admin_backup_restore_post:
      properties:
        archive:
          contentMediaType: application/octet-stream
          description: tar.gz produced by GET /download.
          title: Archive
          type: string
      required:
        - archive
      title: Body_restore_backup_endpoint_v1_admin_backup_restore_post
      type: object
    Body_upload_source_archive_endpoint_v1_projects__project_id__source_archive_post:
      properties:
        upload:
          contentMediaType: application/octet-stream
          description: A .zip archive of the project source tree.
          title: Upload
          type: string
      required:
        - upload
      title: >-
        Body_upload_source_archive_endpoint_v1_projects__project_id__source_archive_post
      type: object
    ComplianceAffectedComponent:
      description: |-
        A component_version that carries the row's license — preview shape.

        The unified grid embeds at most 5 affected components per row as a
        preview chip strip. The full list ships via the existing license drawer
        (``GET /v1/license_findings/{id}``) — the unified grid keys its drawer
        on the same ``license_finding_id`` so the existing drawer is reused
        verbatim.
      properties:
        component_version_id:
          format: uuid
          title: Component Version Id
          type: string
        name:
          description: Component name (without version).
          title: Name
          type: string
        purl:
          anyOf:
            - type: string
            - type: 'null'
          description: Package URL including version, when known.
          title: Purl
        version:
          title: Version
          type: string
      required:
        - component_version_id
        - name
        - version
      title: ComplianceAffectedComponent
      type: object
    ComplianceListResponse:
      description: Page of compliance rows + the project-wide category distribution.
      properties:
        distribution:
          $ref: '#/components/schemas/LicenseDistribution'
          description: >-
            Unfiltered category counts for the underlying scan. Stable across
            pagination + filters so the chart axis does not jump.
        generated_at:
          description: >-
            Server clock when the response was assembled. Echoed so clients can
            tag a cached page.
          format: date-time
          title: Generated At
          type: string
        items:
          items:
            $ref: '#/components/schemas/ComplianceRow'
          title: Items
          type: array
        limit:
          maximum: 500
          minimum: 1
          title: Limit
          type: integer
        offset:
          minimum: 0
          title: Offset
          type: integer
        total:
          description: Total rows matching the active filter, pre-pagination.
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - distribution
        - total
        - limit
        - offset
        - generated_at
      title: ComplianceListResponse
      type: object
    ComplianceObligation:
      description: >-
        One obligation attached to the row's license.


        The unified grid embeds the obligation summary inline so the user does

        not need to open a drawer to see "what does this license require?". The

        drawer still exists for the full text + reference URL — keyed by

        ``obligation_id`` so the existing ``GET
        /v1/projects/{id}/obligations/{id}``

        payload is reused verbatim.
      properties:
        kind:
          description: >-
            Obligation kind — free-form catalog string (attribution,
            source-disclosure, copyleft, ...).
          maxLength: 64
          title: Kind
          type: string
        obligation_id:
          format: uuid
          title: Obligation Id
          type: string
        summary:
          description: >-
            Short human-readable summary of the obligation. Capped at 240 chars
            by the service so the grid row stays compact.
          title: Summary
          type: string
      required:
        - obligation_id
        - kind
        - summary
      title: ComplianceObligation
      type: object
    ComplianceRow:
      description: |-
        One row in the unified Compliance grid.

        A row aggregates "one license in the latest scan" with:
          - its category (allowed / conditional / forbidden / unknown);
          - the components affected by it (preview + total count);
          - the obligations attached to it (inline summaries);
          - whether the license requires a NOTICE entry.

        ``license_finding_id`` is the same opaque handle the existing License
        drawer uses — the grid's row-click → drawer cycle keys on this id so the
        drawer renders without a new endpoint.
      properties:
        affected_component_count:
          description: >-
            Distinct component_versions in the latest scan that carry this
            license.
          minimum: 0
          title: Affected Component Count
          type: integer
        affected_components:
          description: >-
            Preview of up to 5 affected component_versions, ordered by name +
            version. Use ``affected_component_count`` for the true total — the
            preview is capped so the grid row stays compact. The License drawer
            renders the full list.
          items:
            $ref: '#/components/schemas/ComplianceAffectedComponent'
          title: Affected Components
          type: array
        category:
          enum:
            - allowed
            - conditional
            - forbidden
            - unknown
          title: Category
          type: string
        category_override_source:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Reserved for v2.5+ policy-override surfacing. Today always ``null``;
            clients should render an override badge only when the value is
            non-null.
          title: Category Override Source
        category_source:
          default: static
          description: >-
            Where the row's category came from. ``static`` = catalog default
            (``licenses.category``). Reserved values ``policy``/``compound``
            anticipate policy-override semantics (v2.5+); today the service
            always emits ``static``.
          title: Category Source
          type: string
        kind:
          description: >-
            ORT classification kind on the representative finding (declared /
            concluded / detected).
          enum:
            - declared
            - concluded
            - detected
          title: Kind
          type: string
        license_finding_id:
          description: >-
            license_findings.id of a representative finding for this license in
            the latest scan. Same handle the existing License drawer (GET
            /v1/license_findings/{id}) accepts.
          format: uuid
          title: License Finding Id
          type: string
        license_id:
          description: licenses.id (catalog row).
          format: uuid
          title: License Id
          type: string
        license_name:
          title: License Name
          type: string
        notice_required:
          default: false
          description: >-
            True when this license carries an ``attribution`` or ``notice``
            obligation — the project owes a NOTICE entry for it. Derived from
            ``obligations`` so the grid can flag rows without a second trip.
          title: Notice Required
          type: boolean
        obligations:
          description: >-
            Obligations attached to this license, ordered by kind. Empty when
            the catalog records none for this license.
          items:
            $ref: '#/components/schemas/ComplianceObligation'
          title: Obligations
          type: array
        spdx_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            SPDX short identifier (MIT, Apache-2.0, GPL-3.0-only, ...). Null for
            ORT custom licenses (LicenseRef-*).
          title: Spdx Id
      required:
        - license_finding_id
        - license_id
        - license_name
        - category
        - kind
        - affected_component_count
      title: ComplianceRow
      type: object
    ComponentDetailResponse:
      description: Drawer payload for a single component in a project's latest scan.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        dependency_scope:
          anyOf:
            - enum:
                - required
                - optional
              type: string
            - type: 'null'
          description: >-
            W2 #31 — BD-style 'Usage' for the chosen (shallowest) path. ``null``
            when cdxgen left the field unset on that path. Drawers surface the
            row's own scope rather than an aggregate, because depth/direct
            already pin one path.
          title: Dependency Scope
        depth:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: >-
            Shortest-path distance from a dependency-graph root (v2.2): 1 =
            direct, 2+ = transitive. ``null`` when the scan carried no
            dependency graph.
          title: Depth
        direct:
          default: false
          description: True when this is a direct dependency (graph depth 1).
          title: Direct
          type: boolean
        id:
          format: uuid
          title: Id
          type: string
        license:
          anyOf:
            - type: string
            - type: 'null'
          title: License
        license_category:
          enum:
            - forbidden
            - conditional
            - allowed
            - unknown
          title: License Category
          type: string
        name:
          title: Name
          type: string
        obligations:
          description: >-
            M-20 — duties carried by every license observed for this component
            in the anchoring scan, ordered by (kind, license, id) for a
            deterministic response. Empty when the component has no license, the
            license is not in the catalog, or the catalog defines no obligations
            for it.
          items:
            $ref: '#/components/schemas/ObligationRef'
          title: Obligations
          type: array
        project_id:
          format: uuid
          title: Project Id
          type: string
        purl:
          anyOf:
            - type: string
            - type: 'null'
          title: Purl
        raw_data:
          additionalProperties: true
          title: Raw Data
          type: object
        severity_max:
          enum:
            - critical
            - high
            - medium
            - low
            - info
            - none
          title: Severity Max
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
        version:
          title: Version
          type: string
        vulnerabilities:
          items:
            $ref: '#/components/schemas/VulnerabilityRef'
          title: Vulnerabilities
          type: array
      required:
        - id
        - project_id
        - name
        - version
        - license_category
        - severity_max
        - created_at
        - updated_at
      title: ComponentDetailResponse
      type: object
    ComponentListResponse:
      description: Page of components for a project, derived from its latest scan.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ComponentSummary'
          title: Items
          type: array
        limit:
          title: Limit
          type: integer
        offset:
          title: Offset
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - limit
        - offset
      title: ComponentListResponse
      type: object
    ComponentSummary:
      description: One row in the components tab list. Optimized for table rendering.
      properties:
        component_id:
          format: uuid
          title: Component Id
          type: string
        dependency_scope:
          anyOf:
            - enum:
                - required
                - optional
              type: string
            - type: 'null'
          description: >-
            W2 #31 — BD-style 'Usage' for the component, derived from the
            CycloneDX ``component.scope`` field cdxgen emits. The value is
            aggregated across the same cv's dependency paths (``required`` wins
            over ``optional`` — a component used at runtime from any path is
            reported as ``required``). ``null`` when every path left scope unset
            (the common case for ecosystems whose SBOMs do not encode scope) —
            the UI renders that as '—' rather than guessing.
          title: Dependency Scope
        depth:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: >-
            Shortest-path distance from a dependency-graph root (v2.2): 1 =
            direct, 2+ = transitive. The shallowest path when the component
            appears at several. ``null`` when the scan carried no dependency
            graph (older scans / flat-list ecosystems).
          title: Depth
        direct:
          default: false
          description: >-
            True when this component is a direct dependency of the scanned
            project (graph depth 1). False for transitive deps and when the
            graph was unavailable.
          title: Direct
          type: boolean
        id:
          description: component_version id (the scan-bound row)
          format: uuid
          title: Id
          type: string
        license:
          anyOf:
            - type: string
            - type: 'null'
          description: SPDX id of the worst-category license, or its name if no SPDX id.
          title: License
        license_category:
          enum:
            - forbidden
            - conditional
            - allowed
            - unknown
          title: License Category
          type: string
        name:
          title: Name
          type: string
        purl:
          anyOf:
            - type: string
            - type: 'null'
          title: Purl
        severity_max:
          enum:
            - critical
            - high
            - medium
            - low
            - info
            - none
          title: Severity Max
          type: string
        version:
          title: Version
          type: string
        vulnerability_count:
          minimum: 0
          title: Vulnerability Count
          type: integer
      required:
        - id
        - component_id
        - name
        - version
        - license_category
        - severity_max
        - vulnerability_count
      title: ComponentSummary
      type: object
    DashboardSummary:
      description: >-
        Portfolio overview for the caller's accessible projects.


        All aggregates are scoped to projects the caller may read: super-admins
        see

        every project; everyone else sees only projects whose ``team_id`` is one
        of

        their team memberships. A user must never see counts that include
        another

        team's projects.
      example:
        license_category_counts:
          conditional: 4
          permissive: 180
          prohibited: 1
          unknown: 12
        pending_approvals_count: 2
        project_count: 7
        recent_scans:
          - finished_at: '2026-05-25T09:14:00Z'
            kind: source
            project_id: 9a2b7e10-0000-0000-0000-000000000002
            project_name: payments-api
            scan_id: 3f1d8c2a-0000-0000-0000-000000000001
            status: succeeded
        scan_status_counts:
          failed: 2
          queued: 1
          running: 0
          succeeded: 12
        vulnerability_severity_counts:
          critical: 3
          high: 9
          info: 5
          low: 41
          medium: 22
      properties:
        license_category_counts:
          $ref: '#/components/schemas/LicenseCategoryCounts'
        pending_approvals_count:
          description: >-
            Open component approvals (status in {'pending','under_review'}) for
            accessible projects.
          minimum: 0
          title: Pending Approvals Count
          type: integer
        project_count:
          description: Accessible, non-archived projects.
          minimum: 0
          title: Project Count
          type: integer
        recent_scans:
          description: The 10 most recent scans across accessible projects, newest first.
          items:
            $ref: '#/components/schemas/RecentScan'
          title: Recent Scans
          type: array
        scan_status_counts:
          $ref: '#/components/schemas/ScanStatusCounts'
        vulnerability_severity_counts:
          $ref: '#/components/schemas/VulnerabilitySeverityCounts'
      required:
        - project_count
        - pending_approvals_count
      title: DashboardSummary
      type: object
    DependencyChangeOut:
      description: One applied range edit in the proposed manifest.
      examples:
        - after: ^4.17.21
          before: ^4.17.20
          changed: true
          package: lodash
          section: dependencies
      properties:
        after:
          description: The range string after the edit.
          title: After
          type: string
        before:
          description: The range string before the edit.
          title: Before
          type: string
        changed:
          description: Whether the range actually changed.
          title: Changed
          type: boolean
        package:
          description: The npm package name (scoped names kept).
          title: Package
          type: string
        section:
          description: >-
            Manifest block the entry lives in (dependencies / devDependencies /
            optionalDependencies / peerDependencies).
          title: Section
          type: string
      required:
        - package
        - section
        - before
        - after
        - changed
      title: DependencyChangeOut
      type: object
    DiffComponentAdded:
      description: A package present in target but NOT in base (newly introduced).
      properties:
        name:
          title: Name
          type: string
        namespace:
          anyOf:
            - type: string
            - type: 'null'
          title: Namespace
        purl:
          description: The component_version's full purl (purl-with-version) in target.
          title: Purl
          type: string
        version:
          title: Version
          type: string
      required:
        - name
        - purl
        - version
      title: DiffComponentAdded
      type: object
    DiffComponentChanged:
      description: >-
        The same package present in BOTH snapshots at a DIFFERENT version
        (bump).
      properties:
        base_version:
          title: Base Version
          type: string
        name:
          title: Name
          type: string
        namespace:
          anyOf:
            - type: string
            - type: 'null'
          title: Namespace
        purl:
          description: >-
            The component package's purl WITHOUT version (the stable package
            identity shared by base_version and target_version).
          title: Purl
          type: string
        target_version:
          title: Target Version
          type: string
      required:
        - name
        - purl
        - base_version
        - target_version
      title: DiffComponentChanged
      type: object
    DiffComponentRemoved:
      description: >-
        A package present in base but NOT in target (removed, e.g. log4j
        dropped).
      properties:
        name:
          title: Name
          type: string
        namespace:
          anyOf:
            - type: string
            - type: 'null'
          title: Namespace
        purl:
          description: The component_version's full purl (purl-with-version) in base.
          title: Purl
          type: string
        version:
          title: Version
          type: string
      required:
        - name
        - purl
        - version
      title: DiffComponentRemoved
      type: object
    DiffComponents:
      description: The three component change sets between base and target.
      properties:
        added:
          items:
            $ref: '#/components/schemas/DiffComponentAdded'
          title: Added
          type: array
        changed:
          items:
            $ref: '#/components/schemas/DiffComponentChanged'
          title: Changed
          type: array
        removed:
          items:
            $ref: '#/components/schemas/DiffComponentRemoved'
          title: Removed
          type: array
      title: DiffComponents
      type: object
    DiffGateDelta:
      description: The build-gate verdict compared across the two snapshots.
      properties:
        base:
          anyOf:
            - enum:
                - pass
                - fail
              type: string
            - type: 'null'
          title: Base
        target:
          anyOf:
            - enum:
                - pass
                - fail
              type: string
            - type: 'null'
          title: Target
      title: DiffGateDelta
      type: object
    DiffIntDelta:
      description: A single integer metric (a count) compared across the two snapshots.
      properties:
        base:
          default: 0
          minimum: 0
          title: Base
          type: integer
        target:
          default: 0
          minimum: 0
          title: Target
          type: integer
      title: DiffIntDelta
      type: object
    DiffLicenseCategoryDelta:
      description: >-
        Per-license-category component counts compared across the two snapshots.


        Each category carries a ``{base, target}`` pair so the UI can show how
        the

        license-risk distribution shifted between the two releases. Counts are
        the

        number of component_versions whose WORST license category lands in each

        bucket (same worst-per-component bucketing the license distribution
        uses).
      properties:
        conditional:
          $ref: '#/components/schemas/DiffIntDelta'
        permissive:
          $ref: '#/components/schemas/DiffIntDelta'
          description: Allowed/permissive-category components (MIT/Apache-2.0 …).
        prohibited:
          $ref: '#/components/schemas/DiffIntDelta'
          description: Forbidden-category components (GPL/AGPL/SSPL/BUSL …).
        unknown:
          $ref: '#/components/schemas/DiffIntDelta'
      title: DiffLicenseCategoryDelta
      type: object
    DiffLicenses:
      description: License change view — per-category base/target component-count deltas.
      properties:
        category_delta:
          $ref: '#/components/schemas/DiffLicenseCategoryDelta'
      title: DiffLicenses
      type: object
    DiffScalarDelta:
      description: >-
        A single scalar metric compared across the two snapshots (base vs
        target).


        ``base`` / ``target`` may be null when the metric is not aggregable for
        that

        side (e.g. ``risk_score`` if a snapshot is somehow not aggregable —
        should not

        occur for a real succeeded scan).
      properties:
        base:
          anyOf:
            - type: number
            - type: 'null'
          title: Base
        target:
          anyOf:
            - type: number
            - type: 'null'
          title: Target
      title: DiffScalarDelta
      type: object
    DiffSeverityDelta:
      description: >-
        Worst-per-component vuln-severity counts compared across the two
        snapshots.


        Each bucket carries a ``{base, target}`` pair so the UI can render the
        delta.

        Counts are the number of component_versions whose WORST CVE finding
        lands in

        each bucket — the SAME worst-per-component bucketing the Releases table
        /

        Overview tab use, so the diff summary can never disagree with them. Only
        the

        four risk-bearing buckets are surfaced (``info`` / ``none`` are not

        actionable on a summary).
      properties:
        critical:
          $ref: '#/components/schemas/DiffIntDelta'
        high:
          $ref: '#/components/schemas/DiffIntDelta'
        low:
          $ref: '#/components/schemas/DiffIntDelta'
        medium:
          $ref: '#/components/schemas/DiffIntDelta'
      title: DiffSeverityDelta
      type: object
    DiffSnapshotRef:
      description: >-
        One side (base or target) of the comparison — which succeeded scan it
        is.
      properties:
        created_at:
          description: When this scan was created.
          format: date-time
          title: Created At
          type: string
        release:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional release/version label from the scan's ``metadata.release``
            (e.g. 'v0.1'). Non-unique and often absent (null).
          title: Release
        scan_id:
          description: The succeeded scan that IS this side of the diff.
          format: uuid
          title: Scan Id
          type: string
      required:
        - scan_id
        - created_at
      title: DiffSnapshotRef
      type: object
    DiffSummary:
      description: Side-by-side per-snapshot summary (base vs target) for the diff header.
      properties:
        component_count:
          $ref: '#/components/schemas/DiffIntDelta'
          description: Distinct component_versions observed in each snapshot.
        gate:
          $ref: '#/components/schemas/DiffGateDelta'
        risk_score:
          $ref: '#/components/schemas/DiffScalarDelta'
          description: >-
            Overall risk 0–100 (max of security/license axis) for each snapshot,
            using the SAME non-saturating scorer the Overview / Releases use.
        severity:
          $ref: '#/components/schemas/DiffSeverityDelta'
      title: DiffSummary
      type: object
    DiffVulnerabilities:
      description: >-
        Vulnerabilities introduced / resolved between base and target (OPEN
        set).
      properties:
        introduced:
          description: Open in target, not open in base (newly introduced exposure).
          items:
            $ref: '#/components/schemas/DiffVulnerability'
          title: Introduced
          type: array
        resolved:
          description: Was open in base, gone/closed in target (resolved exposure).
          items:
            $ref: '#/components/schemas/DiffVulnerability'
          title: Resolved
          type: array
      title: DiffVulnerabilities
      type: object
    DiffVulnerability:
      description: >-
        One (cve, component_version) finding that changed open-status between
        snapshots.
      properties:
        component_name:
          title: Component Name
          type: string
        component_version:
          title: Component Version
          type: string
        cve_id:
          description: The CVE / advisory external id (e.g. 'CVE-2021-44228').
          title: Cve Id
          type: string
        severity:
          description: The CVE severity bucket (critical/high/medium/low/info/unknown).
          title: Severity
          type: string
      required:
        - cve_id
        - severity
        - component_name
        - component_version
      title: DiffVulnerability
      type: object
    DryRunRecommendationOut:
      description: One npm component the dry-run proposes to bump (advisory).
      examples:
        - current_version: 4.17.20
          package: lodash
          recommended_version: 4.17.21
      properties:
        current_version:
          description: The version the latest scan saw.
          title: Current Version
          type: string
        package:
          description: The npm package name (scoped names kept).
          title: Package
          type: string
        recommended_version:
          description: The minimum-safe upgrade target (from the a3 engine).
          title: Recommended Version
          type: string
      required:
        - package
        - current_version
        - recommended_version
      title: DryRunRecommendationOut
      type: object
    ForgotPasswordRequest:
      description: Inbound payload for POST /auth/forgot-password.
      properties:
        email:
          format: email
          title: Email
          type: string
      required:
        - email
      title: ForgotPasswordRequest
      type: object
    GateResultResponse:
      description: Build-gate verdict for the project's latest successful scan.
      properties:
        critical_cve_count:
          description: >-
            Number of open critical-severity findings on the evaluated scan.
            Open = status not in (not_affected, fixed, false_positive).
          minimum: 0
          title: Critical Cve Count
          type: integer
        epss_gate_count:
          default: 0
          description: >-
            Number of open findings on the evaluated scan whose CVE has an EPSS
            score at or above ``epss_threshold``. Always 0 when the EPSS gate is
            disabled (``epss_threshold == null``).
          minimum: 0
          title: Epss Gate Count
          type: integer
        epss_threshold:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            The active EPSS gate threshold in [0, 1], read from the
            ``GATE_EPSS_THRESHOLD`` environment variable at evaluation time.
            ``null`` when the EPSS gate is disabled (unset/unparseable env), in
            which case the gate behaves exactly as the critical-CVE +
            forbidden-license gate.
          title: Epss Threshold
        evaluated_at:
          description: Server timestamp at which the verdict was computed (UTC, ISO-8601).
          format: date-time
          title: Evaluated At
          type: string
        forbidden_license_count:
          description: >-
            Distinct component_versions on the evaluated scan that carry at
            least one forbidden-classification license.
          minimum: 0
          title: Forbidden License Count
          type: integer
        gate:
          description: >-
            Overall outcome. ``pass`` when no critical CVEs and no forbidden
            licenses are present, otherwise ``fail``.
          enum:
            - pass
            - fail
          title: Gate
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
        reachable_critical_cve_count:
          default: 0
          description: >-
            Subset of the open critical findings on the evaluated scan that an
            analyser has additionally proven REACHABLE (reachable IS TRUE) — a
            v2.3 priority signal. Always populated; ``0`` when no finding is
            proven reachable or no reachability analysis has run. By default
            this does NOT change the verdict (it is informational), unless the
            reachable-only critical mode is enabled (see
            ``reachable_gate_enforced``).
          minimum: 0
          title: Reachable Critical Cve Count
          type: integer
        reachable_gate_enforced:
          default: false
          description: >-
            Whether the opt-in reachable-only critical mode
            (``GATE_REACHABLE_CRITICAL_ONLY`` env) was active for this
            evaluation. When ``false`` (default) every open critical counts —
            the legacy behaviour. When ``true`` the relaxation is requested, but
            it is applied SAFELY: it takes effect only on scans actually
            reachability-analysed (reachability is Go-only today), and even then
            it excludes ONLY criticals PROVEN unreachable — criticals not yet
            analysed (reachable IS NULL) keep blocking. On a scan with no
            reachability analysis the gate falls back to counting every open
            critical, so the flag never silently disables the gate for
            un-analysed ecosystems.
          title: Reachable Gate Enforced
          type: boolean
        reason:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Human-readable explanation when ``gate == 'fail'``. ``null`` for
            passing builds.
          title: Reason
        scan_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          description: >-
            ID of the scan the verdict was computed against. ``null`` when the
            project has never had a successful scan, in which case ``gate ==
            'pass'`` is returned by convention (no signal = no block).
          title: Scan Id
      required:
        - gate
        - critical_cve_count
        - forbidden_license_count
        - project_id
        - evaluated_at
      title: GateResultResponse
      type: object
    GitHubAppCredentialCreateIn:
      description: >-
        Request body for registering a GitHub App credential.


        ``private_key`` is the plaintext PEM — the ONLY place it is accepted. It
        is

        encrypted before persist and never returned.
      properties:
        app_id:
          maxLength: 64
          minLength: 1
          title: App Id
          type: string
        app_slug:
          anyOf:
            - maxLength: 255
              type: string
            - type: 'null'
          title: App Slug
        private_key:
          description: >-
            The GitHub App PEM private key (plaintext). Accepted ONCE at
            registration, encrypted at rest, and never returned.
          minLength: 1
          title: Private Key
          type: string
        webhook_secret:
          anyOf:
            - maxLength: 1024
              type: string
            - type: 'null'
          title: Webhook Secret
      required:
        - app_id
        - private_key
      title: GitHubAppCredentialCreateIn
      type: object
    GitHubAppCredentialListPage:
      description: Paginated list of GitHub App credentials.
      properties:
        items:
          items:
            $ref: '#/components/schemas/GitHubAppCredentialOut'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: GitHubAppCredentialListPage
      type: object
    GitHubAppCredentialOut:
      description: >-
        Response shape for a credential — metadata only, NEVER any key material.


        ``has_private_key`` is always True for a persisted credential (the
        column is

        NOT NULL); it is surfaced explicitly so the UI can render a "configured"

        state without the schema ever carrying the key or its ciphertext.
      properties:
        app_id:
          title: App Id
          type: string
        app_slug:
          anyOf:
            - type: string
            - type: 'null'
          title: App Slug
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        has_private_key:
          title: Has Private Key
          type: boolean
        has_webhook_secret:
          title: Has Webhook Secret
          type: boolean
        id:
          format: uuid
          title: Id
          type: string
        revoked_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Revoked At
        team_id:
          format: uuid
          title: Team Id
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - team_id
        - app_id
        - app_slug
        - has_private_key
        - has_webhook_secret
        - created_by_user_id
        - created_at
        - updated_at
        - revoked_at
      title: GitHubAppCredentialOut
      type: object
    GitHubAppInstallationLinkIn:
      description: Request body for linking (opting-in) an installation to a project.
      properties:
        account_login:
          anyOf:
            - maxLength: 255
              type: string
            - type: 'null'
          title: Account Login
        installation_id:
          maxLength: 64
          minLength: 1
          title: Installation Id
          type: string
        project_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          description: The TrustedOSS project this installation is opted-in to.
          title: Project Id
        repository_full_name:
          anyOf:
            - maxLength: 512
              type: string
            - type: 'null'
          title: Repository Full Name
      required:
        - installation_id
      title: GitHubAppInstallationLinkIn
      type: object
    GitHubAppInstallationListPage:
      description: Paginated list of installations under a credential.
      properties:
        items:
          items:
            $ref: '#/components/schemas/GitHubAppInstallationOut'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: GitHubAppInstallationListPage
      type: object
    GitHubAppInstallationOut:
      description: Installation row — no secrets at all.
      properties:
        account_login:
          anyOf:
            - type: string
            - type: 'null'
          title: Account Login
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        credential_id:
          format: uuid
          title: Credential Id
          type: string
        id:
          format: uuid
          title: Id
          type: string
        installation_id:
          title: Installation Id
          type: string
        project_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Project Id
        repository_full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Repository Full Name
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - credential_id
        - installation_id
        - account_login
        - repository_full_name
        - project_id
        - created_by_user_id
        - created_at
        - updated_at
      title: GitHubAppInstallationOut
      type: object
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          title: Detail
          type: array
      title: HTTPValidationError
      type: object
    HealthComponent:
      description: One probe in the system-health summary.
      properties:
        detail:
          anyOf:
            - type: string
            - type: 'null'
          description: Human-readable explanation of the status (1-line).
          title: Detail
        name:
          enum:
            - postgres
            - redis
            - celery
            - disk
            - active_scans
            - last_24h_errors
          title: Name
          type: string
        status:
          enum:
            - ok
            - degraded
            - down
          title: Status
          type: string
        value:
          anyOf:
            - type: number
            - type: integer
            - type: 'null'
          description: >-
            Numeric reading (e.g. ``celery`` worker count, ``active_scans`` row
            count, ``last_24h_errors`` row count). ``null`` for boolean probes.
          title: Value
      required:
        - name
        - status
      title: HealthComponent
      type: object
    LicenseCategoryCounts:
      description: |-
        Component license verdicts over each project's latest succeeded scan.

        ``prohibited`` is the UI label for the persisted ``forbidden`` category;
        ``permissive`` is the UI label for ``allowed``. ``conditional`` and
        ``unknown`` keep their persisted names.
      properties:
        conditional:
          default: 0
          minimum: 0
          title: Conditional
          type: integer
        permissive:
          default: 0
          minimum: 0
          title: Permissive
          type: integer
        prohibited:
          default: 0
          minimum: 0
          title: Prohibited
          type: integer
        unknown:
          default: 0
          minimum: 0
          title: Unknown
          type: integer
      title: LicenseCategoryCounts
      type: object
    LicenseCategorySummary:
      description: >-
        Per-project license-category counts for the project-list license axis.


        Mirrors :class:`SeveritySummary` but indexed by license category instead

        of CVE severity. Counts are the number of *components*
        (component_versions)

        whose worst license-category rank lands in each bucket, over the
        project's

        latest **succeeded** scan. Buckets follow the dashboard's rank ordering:

        ``forbidden`` (worst) → ``conditional`` → ``allowed`` → ``unknown``. The

        Projects-page "License classification" card collapses each project's

        counts to its worst non-zero bucket; the segment-click filter narrows
        the

        project list to projects whose worst bucket matches.
      properties:
        allowed:
          default: 0
          minimum: 0
          title: Allowed
          type: integer
        conditional:
          default: 0
          minimum: 0
          title: Conditional
          type: integer
        forbidden:
          default: 0
          minimum: 0
          title: Forbidden
          type: integer
        unknown:
          default: 0
          minimum: 0
          title: Unknown
          type: integer
      title: LicenseCategorySummary
      type: object
    LicenseDetailResponse:
      description: Full drawer payload for a single license_findings row.
      properties:
        affected_components:
          description: >-
            All component_versions in the same scan that carry this license,
            across every kind (declared / concluded / detected). Capped at 500
            rows — see ``affected_components_truncated``.
          items:
            $ref: '#/components/schemas/AffectedComponentByLicense'
          title: Affected Components
          type: array
        affected_components_total:
          default: 0
          description: >-
            Total number of distinct component_versions associated with this
            license in the scan, before the response cap is applied.
          minimum: 0
          title: Affected Components Total
          type: integer
        affected_components_truncated:
          default: false
          description: >-
            True when the server truncated ``affected_components`` to its
            500-row cap. Clients should display a notice and optionally fall
            back to the components tab for the full list.
          title: Affected Components Truncated
          type: boolean
        category:
          enum:
            - allowed
            - conditional
            - forbidden
            - unknown
          title: Category
          type: string
        created_at:
          format: date-time
          title: Created At
          type: string
        finding_kind:
          description: ORT classification kind on this specific finding row.
          enum:
            - declared
            - concluded
            - detected
          title: Finding Kind
          type: string
        id:
          description: license_findings.id (the row the URL points at).
          format: uuid
          title: Id
          type: string
        is_deprecated_license_id:
          default: false
          title: Is Deprecated License Id
          type: boolean
        is_fsf_libre:
          default: false
          title: Is Fsf Libre
          type: boolean
        is_osi_approved:
          default: false
          title: Is Osi Approved
          type: boolean
        license_id:
          format: uuid
          title: License Id
          type: string
        name:
          title: Name
          type: string
        ort_match:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          description: >-
            Best-effort pass-through of license_findings.raw_data. The shape is
            not contractual: ORT may include matched-text excerpts,
            license-detector confidence, copyright statements, and similar.
            Frontends should render this defensively. ``null`` when the scan
            pipeline did not emit any raw data for the finding.
          title: Ort Match
        reference_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Reference Url
        spdx_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Spdx Id
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - license_id
        - name
        - category
        - finding_kind
        - created_at
        - updated_at
      title: LicenseDetailResponse
      type: object
    LicenseDistribution:
      description: >-
        Counts of distinct component_versions per category in the latest scan.


        Always returns all four buckets (zero if absent) so the bar chart
        renders

        a stable axis. This is the single source of truth shared with the
        Overview

        tab's ``license_distribution`` — no duplicate aggregator endpoint.
      properties:
        allowed:
          default: 0
          minimum: 0
          title: Allowed
          type: integer
        conditional:
          default: 0
          minimum: 0
          title: Conditional
          type: integer
        forbidden:
          default: 0
          minimum: 0
          title: Forbidden
          type: integer
        unknown:
          default: 0
          minimum: 0
          title: Unknown
          type: integer
      title: LicenseDistribution
      type: object
    LicenseException:
      additionalProperties: false
      description: >-
        One explicit allow-regardless-of-category waiver.


        ``spdx_id`` + ``reason`` are required. ``expires_at`` (optional) lets c2

        treat an expired waiver as absent. ``component_purl`` (optional) scopes
        the

        waiver to a single component; absent → applies to any component carrying

        ``spdx_id``. Extra keys are rejected so a typo cannot silently smuggle
        an

        un-validated field through the JSONB column.
      properties:
        component_purl:
          anyOf:
            - maxLength: 1024
              type: string
            - type: 'null'
          title: Component Purl
        expires_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Expires At
        reason:
          maxLength: 1000
          minLength: 1
          title: Reason
          type: string
        spdx_id:
          maxLength: 128
          minLength: 1
          title: Spdx Id
          type: string
      required:
        - spdx_id
        - reason
      title: LicenseException
      type: object
    LicenseListItem:
      description: >-
        One row in the Licenses tab table.


        A row represents a single license observed in the project's latest scan,

        aggregated across all component_versions that carry it. ``id`` carries
        the

        license_findings row id of a representative finding (usually the

        lexicographically first by source_path) so the drawer endpoint has a

        stable handle to dereference.
      properties:
        affected_count:
          description: >-
            Distinct component_versions in the latest scan that carry this
            license.
          minimum: 1
          title: Affected Count
          type: integer
        category:
          enum:
            - allowed
            - conditional
            - forbidden
            - unknown
          title: Category
          type: string
        id:
          description: >-
            license_findings.id of a representative finding for this license in
            the latest scan. Used as the drawer's primary key.
          format: uuid
          title: Id
          type: string
        is_fsf_libre:
          default: false
          title: Is Fsf Libre
          type: boolean
        is_osi_approved:
          default: false
          title: Is Osi Approved
          type: boolean
        kind:
          description: >-
            ORT classification kind on the representative finding (declared /
            concluded / detected).
          enum:
            - declared
            - concluded
            - detected
          title: Kind
          type: string
        license_id:
          description: licenses.id (catalog row).
          format: uuid
          title: License Id
          type: string
        name:
          title: Name
          type: string
        sample_finding_id:
          description: >-
            Finding row to pass to GET /v1/license_findings/{id} when the user
            opens the drawer. Echoes ``id`` today; a separate field is kept so
            frontends can stay forward-compatible if the list shape ever needs
            to advertise multiple sample findings.
          format: uuid
          title: Sample Finding Id
          type: string
        spdx_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            SPDX short identifier (MIT, Apache-2.0, GPL-3.0-only, ...). Null for
            ORT custom licenses (LicenseRef-*).
          title: Spdx Id
      required:
        - id
        - license_id
        - name
        - category
        - kind
        - affected_count
        - sample_finding_id
      title: LicenseListItem
      type: object
    LicenseListResponse:
      description: Page of licenses for the project's latest scan.
      properties:
        distribution:
          $ref: '#/components/schemas/LicenseDistribution'
        items:
          items:
            $ref: '#/components/schemas/LicenseListItem'
          title: Items
          type: array
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - distribution
        - total
      title: LicenseListResponse
      type: object
    LicenseMatch:
      description: A per-line license match projected from the folded scancode JSON.
      examples:
        - end_line: 21
          score: 99.5
          spdx_id: MIT
          start_line: 1
      properties:
        end_line:
          description: 1-based last line of the match (inclusive).
          minimum: 1
          title: End Line
          type: integer
        score:
          anyOf:
            - type: number
            - type: 'null'
          description: scancode match score (0-100), or null when unreported.
          title: Score
        spdx_id:
          description: SPDX identifier of the matched license.
          title: Spdx Id
          type: string
        start_line:
          description: 1-based first line of the match (inclusive).
          minimum: 1
          title: Start Line
          type: integer
      required:
        - spdx_id
        - start_line
        - end_line
      title: LicenseMatch
      type: object
    LicensePolicyListPage:
      description: Paginated list of license policies.
      properties:
        items:
          items:
            $ref: '#/components/schemas/LicensePolicyOut'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: LicensePolicyListPage
      type: object
    LicensePolicyOut:
      description: ORM-derived response shape for a single license policy.
      properties:
        category_overrides:
          additionalProperties:
            type: string
          title: Category Overrides
          type: object
        compound_operator_strategy:
          additionalProperties:
            type: string
          title: Compound Operator Strategy
          type: object
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        enabled:
          title: Enabled
          type: boolean
        id:
          format: uuid
          title: Id
          type: string
        license_exceptions:
          items:
            additionalProperties: true
            type: object
          title: License Exceptions
          type: array
        name:
          anyOf:
            - type: string
            - type: 'null'
          title: Name
        organization_id:
          format: uuid
          title: Organization Id
          type: string
        team_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Team Id
        unknown_license_category:
          title: Unknown License Category
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - organization_id
        - team_id
        - name
        - category_overrides
        - license_exceptions
        - unknown_license_category
        - compound_operator_strategy
        - enabled
        - created_by_user_id
        - created_at
        - updated_at
      title: LicensePolicyOut
      type: object
    LicensePolicyUpsertIn:
      additionalProperties: false
      description: |-
        Request body for PUT (upsert) of a team or org license policy.

        All fields are optional with sensible defaults so a minimal ``{}`` body
        creates an empty (effectively no-op) policy. Strict validation rejects
        oversized / malformed maps with a 422 (never a 500).
      properties:
        category_overrides:
          additionalProperties:
            enum:
              - allowed
              - conditional
              - forbidden
            type: string
          title: Category Overrides
          type: object
        compound_operator_strategy:
          additionalProperties:
            enum:
              - most_restrictive
              - least_restrictive
            type: string
          propertyNames:
            enum:
              - AND
              - OR
              - WITH
          title: Compound Operator Strategy
          type: object
        enabled:
          default: true
          title: Enabled
          type: boolean
        license_exceptions:
          items:
            $ref: '#/components/schemas/LicenseException'
          title: License Exceptions
          type: array
        name:
          anyOf:
            - maxLength: 120
              type: string
            - type: 'null'
          title: Name
        unknown_license_category:
          default: conditional
          enum:
            - allowed
            - conditional
            - forbidden
          title: Unknown License Category
          type: string
      title: LicensePolicyUpsertIn
      type: object
    LoginRequest:
      description: Inbound payload for POST /auth/login.
      properties:
        email:
          format: email
          title: Email
          type: string
        password:
          maxLength: 256
          minLength: 1
          title: Password
          type: string
      required:
        - email
        - password
      title: LoginRequest
      type: object
    MembershipPublic:
      description: One of the authenticated user's team memberships (for /auth/me).
      properties:
        role:
          title: Role
          type: string
        team_id:
          format: uuid
          title: Team Id
          type: string
        team_name:
          title: Team Name
          type: string
      required:
        - team_id
        - team_name
        - role
      title: MembershipPublic
      type: object
    NotificationListResponse:
      description: |-
        Paginated list of notifications + the bell badge count.

        ``unread_count`` is the global unread count for the caller (NOT the
        unread count within this page) — the SPA renders it as a badge in the
        nav, independent of the drawer's pagination state.
      properties:
        items:
          items:
            $ref: '#/components/schemas/NotificationOut'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
        unread_count:
          title: Unread Count
          type: integer
      required:
        - items
        - total
        - unread_count
        - page
        - page_size
      title: NotificationListResponse
      type: object
    NotificationOut:
      description: |-
        One row in the notifications list response.

        Frozen contract — the SPA's notification drawer depends on every field
        name and shape. Add new optional fields as nullable; do not rename.
      properties:
        body:
          title: Body
          type: string
        created_at:
          format: date-time
          title: Created At
          type: string
        id:
          format: uuid
          title: Id
          type: string
        kind:
          enum:
            - scan_completed
            - scan_failed
            - cve_detected
            - license_violation
            - approval_pending
            - policy_gate_failed
            - approval_state_changed
          title: Kind
          type: string
        link:
          anyOf:
            - type: string
            - type: 'null'
          title: Link
        read_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Read At
        target_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Target Id
        target_table:
          anyOf:
            - type: string
            - type: 'null'
          title: Target Table
        title:
          title: Title
          type: string
      required:
        - id
        - kind
        - title
        - body
        - link
        - target_table
        - target_id
        - read_at
        - created_at
      title: NotificationOut
      type: object
    NotificationPrefsIn:
      description: |-
        Per-user notification channel toggles (PUT body — full row).

        The endpoint is PUT, not PATCH: every channel must be supplied. Sending
        a partial body would be a 422 (Pydantic enforces required fields).
      properties:
        email_enabled:
          title: Email Enabled
          type: boolean
        in_app_enabled:
          title: In App Enabled
          type: boolean
        slack_enabled:
          title: Slack Enabled
          type: boolean
        teams_enabled:
          title: Teams Enabled
          type: boolean
      required:
        - email_enabled
        - slack_enabled
        - teams_enabled
        - in_app_enabled
      title: NotificationPrefsIn
      type: object
    NotificationPrefsOut:
      description: Per-user notification channel toggles (read shape).
      properties:
        email_enabled:
          title: Email Enabled
          type: boolean
        in_app_enabled:
          title: In App Enabled
          type: boolean
        slack_enabled:
          title: Slack Enabled
          type: boolean
        teams_enabled:
          title: Teams Enabled
          type: boolean
      required:
        - email_enabled
        - slack_enabled
        - teams_enabled
        - in_app_enabled
      title: NotificationPrefsOut
      type: object
    NpmDryRunRequest:
      description: |-
        Optional uploaded ``package.json`` body.

        When omitted, the endpoint best-effort fetches the manifest from the
        project's latest preserved scan source. Supplying ``manifest`` is the
        reliable path when the source was never preserved (or was swept).
      examples:
        - manifest: |
            {
              "name": "demo",
              "dependencies": {
                "lodash": "^4.17.20"
              }
            }
      properties:
        manifest:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Raw package.json text to edit. When omitted, the endpoint reads the
            manifest from the latest preserved scan source (best-effort).
          title: Manifest
      title: NpmDryRunRequest
      type: object
    NpmDryRunResponse:
      description: The computed npm remediation dry-run.
      examples:
        - changed: true
          changes:
            - after: ^4.17.21
              before: ^4.17.20
              changed: true
              package: lodash
              section: dependencies
          ecosystem: npm
          edited_manifest: |
            {
              "name": "demo",
              "dependencies": {
                "lodash": "^4.17.21"
              }
            }
          manifest_found: true
          manifest_source: preserved_source
          notes: []
          project_id: 5b8f1c2e-0c2a-4a1e-9c3d-9c2b1a0f7e11
          recommendations:
            - current_version: 4.17.20
              package: lodash
              recommended_version: 4.17.21
          scan_id: 7a1d2c3b-4e5f-6a7b-8c9d-0e1f2a3b4c5d
          warnings:
            - code: lockfile_regeneration_required
              detail: run `npm install` to regenerate package-lock.json
      properties:
        changed:
          description: True iff at least one dependency range was rewritten.
          title: Changed
          type: boolean
        changes:
          description: The applied range edits.
          items:
            $ref: '#/components/schemas/DependencyChangeOut'
          title: Changes
          type: array
        ecosystem:
          description: Always 'npm' for this endpoint.
          title: Ecosystem
          type: string
        edited_manifest:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            The proposed edited package.json text (only present when changed);
            the lockfile is NOT edited — regenerate it with `npm install`.
          title: Edited Manifest
        manifest_found:
          description: True iff a manifest was available to edit.
          title: Manifest Found
          type: boolean
        manifest_source:
          description: >-
            Where the manifest came from: 'override' (request body),
            'preserved_source' (latest scan tarball), or 'none' (not available).
          enum:
            - override
            - preserved_source
            - none
          title: Manifest Source
          type: string
        notes:
          description: High-level notes about the dry-run (e.g. 'no manifest found').
          items:
            type: string
          title: Notes
          type: array
        project_id:
          description: The project the dry-run is for.
          format: uuid
          title: Project Id
          type: string
        recommendations:
          description: The npm upgrade recommendations considered for the edit.
          items:
            $ref: '#/components/schemas/DryRunRecommendationOut'
          title: Recommendations
          type: array
        scan_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          description: The scan the recommendations were derived from (latest scan).
          title: Scan Id
        warnings:
          description: Non-fatal notes (skipped packages, lockfile guidance, …).
          items:
            $ref: '#/components/schemas/RemediationWarningOut'
          title: Warnings
          type: array
      required:
        - project_id
        - ecosystem
        - manifest_source
        - manifest_found
        - changed
      title: NpmDryRunResponse
      type: object
    NpmPullRequestCreate:
      description: >-
        Optional uploaded ``package.json`` body (mirrors the b2 dry-run
        request).


        When omitted, the service best-effort fetches the manifest from the
        project's

        latest preserved scan source. NOTE: the target repository is NOT part of
        this

        body — it is derived from the project's opted-in GitHub App
        installation, so a

        caller can never point the PR at an arbitrary repo.
      examples:
        - manifest: |
            {
              "name": "demo",
              "dependencies": {
                "lodash": "^4.17.20"
              }
            }
      properties:
        manifest:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Raw package.json text to edit. When omitted, the service reads the
            manifest from the latest preserved scan source (best-effort).
          title: Manifest
      title: NpmPullRequestCreate
      type: object
    OAuthIdentityListResponse:
      description: |-
        Response wrapper for ``GET /v1/users/me/oauth-identities``.

        Sorted oldest-first; the caller renders the list as "first connected
        on T". The response has no pagination — a single user is unlikely to
        accumulate enough identities to need it (GitHub + Google + maybe a
        future SSO IdP).

        ``has_password`` (M-16, additive — frozen contract allows new fields,
        never renames): whether the caller has a usable password. Lets the
        profile page pre-disable the Unlink button on the last identity of an
        OAuth-only account instead of surfacing the server's 409
        (``oauth_unlink_blocks_login``) after the click. The boolean is
        computed with the SAME criterion the 409 guard uses
        (:func:`services.oauth_identity_service._password_is_set` — NULL and
        empty string both count as "no password"). The raw ``hashed_password``
        is never serialised — only this boolean.
      properties:
        has_password:
          title: Has Password
          type: boolean
        items:
          items:
            $ref: '#/components/schemas/OAuthIdentityOut'
          title: Items
          type: array
      required:
        - items
        - has_password
      title: OAuthIdentityListResponse
      type: object
    OAuthIdentityOut:
      description: |-
        One linked OAuth identity in the self-service list response.

        Frozen contract — every field name + shape is depended on by the SPA
        profile page. Add new fields as nullable; never rename.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        id:
          format: uuid
          title: Id
          type: string
        provider:
          enum:
            - github
            - google
          title: Provider
          type: string
        provider_email:
          anyOf:
            - type: string
            - type: 'null'
          title: Provider Email
        provider_user_id:
          title: Provider User Id
          type: string
      required:
        - id
        - provider
        - provider_user_id
        - created_at
      title: OAuthIdentityOut
      type: object
    OAuthProviderStatusOut:
      description: |-
        Availability of a single OAuth provider for sign-in.

        ``configured`` is ``True`` only when both the client id and client
        secret are set — the precondition for the /authorize flow to work
        (see :func:`services.oauth_service.oauth_provider_configured`).
      properties:
        configured:
          title: Configured
          type: boolean
        provider:
          enum:
            - github
            - google
          title: Provider
          type: string
      required:
        - provider
        - configured
      title: OAuthProviderStatusOut
      type: object
    OAuthProvidersResponse:
      description: |-
        Response wrapper for ``GET /auth/oauth/providers``.

        Always lists every supported provider (stable order: github, google),
        each with a bare ``configured`` boolean, so the /login page can decide
        which sign-in buttons to render BEFORE the user authenticates.
      properties:
        providers:
          items:
            $ref: '#/components/schemas/OAuthProviderStatusOut'
          title: Providers
          type: array
      required:
        - providers
      title: OAuthProvidersResponse
      type: object
    ObligationDetailResponse:
      description: Full drawer payload for a single obligation, scoped to a project.
      properties:
        affected_components:
          description: >-
            All component_versions in the project's latest scan that carry the
            parent license. Capped at 500 rows — see
            ``affected_components_truncated``.
          items:
            $ref: '#/components/schemas/AffectedComponentByObligation'
          title: Affected Components
          type: array
        affected_components_total:
          default: 0
          description: >-
            Total number of distinct component_versions associated with the
            parent license in the scan, before the response cap is applied.
          minimum: 0
          title: Affected Components Total
          type: integer
        affected_components_truncated:
          default: false
          description: >-
            True when the server truncated ``affected_components`` to its
            500-row cap. Clients should display a notice and optionally fall
            back to the components tab for the full list.
          title: Affected Components Truncated
          type: boolean
        created_at:
          format: date-time
          title: Created At
          type: string
        id:
          format: uuid
          title: Id
          type: string
        kind:
          maxLength: 64
          title: Kind
          type: string
        license_category:
          enum:
            - allowed
            - conditional
            - forbidden
            - unknown
          title: License Category
          type: string
        license_id:
          format: uuid
          title: License Id
          type: string
        license_name:
          title: License Name
          type: string
        license_reference_url:
          anyOf:
            - type: string
            - type: 'null'
          description: licenses.reference_url for further reading.
          title: License Reference Url
        license_spdx_id:
          anyOf:
            - type: string
            - type: 'null'
          title: License Spdx Id
        link:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional URL provided by the catalog. Frontends MUST scheme-filter
            to http/https before rendering as a clickable link.
          title: Link
        text:
          description: >-
            Human-readable obligation text. Capped at 65 536 bytes — see
            ``text_truncated``.
          title: Text
          type: string
        text_truncated:
          default: false
          description: >-
            True when the server truncated ``text`` to its 65 536-byte cap.
            Clients should display a notice and offer a link to the source
            catalog if available.
          title: Text Truncated
          type: boolean
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - license_id
        - license_name
        - license_category
        - kind
        - text
        - created_at
        - updated_at
      title: ObligationDetailResponse
      type: object
    ObligationListItem:
      description: |-
        One row in the Obligations tab table.

        A row is a single ``(license, obligation_kind)`` pair observed in the
        project's latest scan. ``affected_count`` counts distinct
        component_versions that carry the parent license — not the obligation
        itself, which is a per-license policy attribute. The drawer dereferences
        by ``id`` (the obligation row) within the project scope.
      properties:
        affected_count:
          description: >-
            Distinct component_versions in the latest scan that carry the parent
            license.
          minimum: 0
          title: Affected Count
          type: integer
        id:
          description: obligations.id (catalog row).
          format: uuid
          title: Id
          type: string
        kind:
          description: >-
            Obligation kind — free-form catalog string (e.g. attribution,
            source-disclosure, copyleft). See KNOWN_OBLIGATION_KINDS for the
            ranked allow-list rendered first in the distribution.
          maxLength: 64
          title: Kind
          type: string
        license_category:
          enum:
            - allowed
            - conditional
            - forbidden
            - unknown
          title: License Category
          type: string
        license_id:
          description: licenses.id this obligation belongs to.
          format: uuid
          title: License Id
          type: string
        license_name:
          description: Full name of the parent license.
          title: License Name
          type: string
        license_spdx_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            SPDX short identifier of the parent license. Null for ORT custom
            licenses (LicenseRef-*).
          title: License Spdx Id
        link:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional URL with further explanation of the obligation. Frontends
            MUST scheme-filter this before rendering as a link.
          title: Link
        text:
          description: Human-readable obligation text.
          title: Text
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - license_id
        - license_name
        - license_category
        - kind
        - text
        - affected_count
        - updated_at
      title: ObligationListItem
      type: object
    ObligationListResponse:
      description: |-
        Page of obligations for the project's latest scan.

        ``distribution`` is the per-kind count across all obligations visible in
        the latest scan, regardless of the active filter. Known kinds appear
        first in their canonical order; unknown kinds (catalog growth) are
        appended alphabetically. Always populated even on empty result sets.
      properties:
        distribution:
          additionalProperties:
            type: integer
          description: >-
            kind → count of distinct (license, kind) obligation rows in the
            latest scan. Known kinds appear first in canonical order, followed
            by unknown kinds alphabetically.
          title: Distribution
          type: object
        items:
          items:
            $ref: '#/components/schemas/ObligationListItem'
          title: Items
          type: array
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - distribution
        - total
      title: ObligationListResponse
      type: object
    ObligationRef:
      description: |-
        Compact license-obligation reference attached to a component detail.

        M-20 — the Components drawer renders the duties carried by the
        component's license(s) without a second request. This is a deliberately
        lean projection of the obligations catalog: the full drawer shape
        (affected components, truncation flags, …) stays on
        :class:`schemas.obligation_detail.ObligationDetailResponse`, reachable
        from the Obligations tab.
      properties:
        id:
          description: obligations.id (catalog row).
          format: uuid
          title: Id
          type: string
        kind:
          description: >-
            Obligation kind — free-form catalog string (e.g. attribution,
            source-disclosure, copyleft).
          maxLength: 64
          title: Kind
          type: string
        license:
          description: >-
            Display identifier of the parent license: its SPDX short id, falling
            back to the license name for ORT custom licenses (LicenseRef-*) that
            carry no SPDX id.
          title: License
          type: string
        link:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional URL with further explanation. Frontends MUST scheme-filter
            to http/https before rendering as a clickable link.
          title: Link
        text:
          description: Human-readable obligation text.
          title: Text
          type: string
      required:
        - id
        - kind
        - text
        - license
      title: ObligationRef
      type: object
    PostPRCommentRequest:
      description: CI-side input for ``POST /v1/scans/{scan_id}/post-pr-comment``.
      properties:
        dry_run:
          default: false
          description: >-
            When ``true`` the endpoint builds the Markdown comment but does not
            call GitHub. Useful for local CI rehearsals and used by the default
            integration tests so they do not require network access.
          title: Dry Run
          type: boolean
        pr_number:
          description: GitHub PR number.
          maximum: 10000000
          minimum: 1
          title: Pr Number
          type: integer
        repo_full_name:
          description: >-
            GitHub ``owner/repo`` slug. Validated against the GitHub naming
            rules so we never call api.github.com with attacker-controlled path
            segments.
          maxLength: 140
          minLength: 3
          title: Repo Full Name
          type: string
      required:
        - repo_full_name
        - pr_number
      title: PostPRCommentRequest
      type: object
    PostPRCommentResponse:
      description: Outcome of a PR-comment post.
      properties:
        body_preview:
          description: >-
            The first 280 characters of the rendered comment body. The full body
            is never returned because it can grow large; the preview is enough
            for the CI runner to log a sanity check.
          title: Body Preview
          type: string
        comment_id:
          anyOf:
            - type: integer
            - type: 'null'
          description: >-
            GitHub issue-comment id. ``null`` for ``dry_run`` and on transport
            errors that we choose not to surface to the caller.
          title: Comment Id
        comment_url:
          anyOf:
            - type: string
            - type: 'null'
          description: '``html_url`` of the comment on github.com.'
          title: Comment Url
        gate:
          description: >-
            Echo of the gate verdict the comment reports. Lets the CI runner
            branch on the build-blocking decision in a single round-trip.
          enum:
            - pass
            - fail
          title: Gate
          type: string
        status:
          enum:
            - posted
            - updated
            - dry_run
          title: Status
          type: string
      required:
        - status
        - body_preview
        - gate
      title: PostPRCommentResponse
      type: object
    ProjectCreate:
      additionalProperties: false
      description: Inbound payload for POST /v1/projects.
      properties:
        default_branch:
          anyOf:
            - maxLength: 255
              type: string
            - type: 'null'
          title: Default Branch
        description:
          anyOf:
            - maxLength: 4000
              type: string
            - type: 'null'
          title: Description
        git_url:
          anyOf:
            - maxLength: 2048
              type: string
            - type: 'null'
          title: Git Url
        name:
          maxLength: 255
          minLength: 1
          title: Name
          type: string
        slug:
          maxLength: 64
          minLength: 1
          title: Slug
          type: string
        team_id:
          format: uuid
          title: Team Id
          type: string
        visibility:
          default: team
          enum:
            - team
            - organization
          title: Visibility
          type: string
      required:
        - team_id
        - name
        - slug
      title: ProjectCreate
      type: object
    ProjectDiff:
      description: The full diff between two succeeded-scan snapshots of one project.
      example:
        base:
          created_at: '2026-05-20T10:00:00Z'
          release: v0.1
          scan_id: 3c15c82f-c409-4f5f-b7d9-92bca8cc1f7f
        components:
          added:
            - name: log4j-core
              namespace: org.apache.logging.log4j
              purl: pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
              version: 2.14.1
          changed:
            - base_version: 4.17.20
              name: lodash
              purl: pkg:npm/lodash
              target_version: 4.17.21
          removed: []
        licenses:
          category_delta:
            conditional:
              base: 0
              target: 2
            permissive:
              base: 0
              target: 80
            prohibited:
              base: 0
              target: 1
            unknown:
              base: 0
              target: 5
        summary:
          component_count:
            base: 0
            target: 88
          gate:
            base: pass
            target: fail
          risk_score:
            base: 0
            target: 92.9
          severity:
            critical:
              base: 0
              target: 10
            high:
              base: 0
              target: 8
            low:
              base: 0
              target: 5
            medium:
              base: 0
              target: 20
        target:
          created_at: '2026-05-22T10:00:00Z'
          scan_id: 50b3d477-2211-47a3-947b-69022dabb2b3
        truncated: false
        vulnerabilities:
          introduced:
            - component_name: log4j-core
              component_version: 2.14.1
              cve_id: CVE-2021-44228
              severity: critical
          resolved: []
      properties:
        base:
          $ref: '#/components/schemas/DiffSnapshotRef'
        components:
          $ref: '#/components/schemas/DiffComponents'
        licenses:
          $ref: '#/components/schemas/DiffLicenses'
        summary:
          $ref: '#/components/schemas/DiffSummary'
        target:
          $ref: '#/components/schemas/DiffSnapshotRef'
        truncated:
          default: false
          description: >-
            True when any change-set list (components added/removed/changed,
            vulnerabilities introduced/resolved) was capped at the defensive
            per-list limit. The summary counts are always exact; only the
            enumerated lists are truncated.
          title: Truncated
          type: boolean
        vulnerabilities:
          $ref: '#/components/schemas/DiffVulnerabilities'
      required:
        - base
        - target
      title: ProjectDiff
      type: object
    ProjectListResponse:
      description: Page of projects + total count for client-side paging UI.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ProjectPublic'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        size:
          title: Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - size
      title: ProjectListResponse
      type: object
    ProjectOverviewResponse:
      description: Aggregated risk / scan picture for the project detail Overview tab.
      properties:
        current_user_role:
          description: >-
            The requesting user's effective role *within this project's owning
            team*: 'super_admin' for platform superusers, otherwise the user's
            membership role on the project's team ('team_admin' / 'developer').
            Users who can read the project via org-wide visibility but hold no
            membership default to the least-privileged 'developer'. The frontend
            uses this (not the global JWT role) to gate team-scoped actions such
            as vulnerability suppression (BUG-005).
          enum:
            - super_admin
            - team_admin
            - developer
          title: Current User Role
          type: string
        has_git_credential:
          default: false
          description: >-
            Feature #18 Part B — True when a private-repo git credential is
            configured for this project. Read-only; the plaintext and ciphertext
            are NEVER returned. The UI uses this to show a 'credential
            configured' badge and to drive the set/rotate/clear control.
          title: Has Git Credential
          type: boolean
        last_scan_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            Timestamp (`created_at`) of the project's latest scan *attempt*
            regardless of status — the attempt timeline. May be a failed scan.
            `null` when the project has never been scanned.
          title: Last Scan At
        last_succeeded_scan_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            Timestamp (`created_at`) of the project's latest *succeeded* scan —
            the scan whose findings this overview (and the SBOM export) actually
            reflect, resolved via the same anchor as the build gate. `null` when
            the project has no succeeded scan. The SBOM tab labels its download
            with THIS field (not `last_scan_at`) so the timestamp matches what
            is downloaded; the two differ whenever the latest attempt failed.
          title: Last Succeeded Scan At
        license_distribution:
          additionalProperties:
            type: integer
          description: >-
            Count of components per license category. Keys are a subset of
            {forbidden, conditional, allowed, unknown}.
          title: License Distribution
          type: object
        license_score:
          description: >-
            License risk 0–100 driven by the worst license category present:
            forbidden→75–100 (build-blocking), conditional→25–49 (review; never
            Critical on its own), unknown→1–24, allowed→0. Same n/(n+4)
            within-band scaling as security_score.
          maximum: 100
          minimum: 0
          title: License Score
          type: number
        project_id:
          format: uuid
          title: Project Id
          type: string
        project_name:
          title: Project Name
          type: string
        recent_scans:
          items:
            $ref: '#/components/schemas/ScanSummary'
          title: Recent Scans
          type: array
        risk_score:
          description: >-
            Overall project risk 0–100 = max(security_score, license_score) —
            the worse of the two axes. Non-saturating (band-by-worst-severity,
            see security_score/license_score). Kept for back-compat and for
            'riskiest project' sorting / release trends.
          maximum: 100
          minimum: 0
          title: Risk Score
          type: number
        security_score:
          description: >-
            Security risk 0–100 driven by the worst CVE severity present:
            critical→75–100, high→50–74, medium→25–49, low→1–24, none→0. The
            count of that severity sets the position within the band (n/(n+4)),
            so the score rises with count without saturating at a hard cap.
          maximum: 100
          minimum: 0
          title: Security Score
          type: number
        severity_distribution:
          additionalProperties:
            type: integer
          description: >-
            Count of components per severity bucket. Keys are a subset of
            {critical, high, medium, low, info, none}. Buckets with zero
            components are still included so frontends can render an empty bar.
          title: Severity Distribution
          type: object
        total_components:
          title: Total Components
          type: integer
        vuln_data_available:
          anyOf:
            - type: boolean
            - type: 'null'
          description: >-
            #35 Surface B — whether the DT vulnerability database held any data
            WHEN the anchored scan ran (captured in scan_metadata at scan time).
            True = the DB was populated, so an empty Security axis is a real
            clean result. False = the DB was empty, so 0 CVEs means 'no data',
            NOT 'safe' — the UI shows a caveat prompting a rescan once the NVD
            mirror finishes. None = unknown (no succeeded scan, or a scan that
            predates this capture); the UI shows no caveat (never cry wolf).
          title: Vuln Data Available
      required:
        - project_id
        - project_name
        - total_components
        - risk_score
        - security_score
        - license_score
        - current_user_role
      title: ProjectOverviewResponse
      type: object
    ProjectPublic:
      description: Outbound shape for every project-bearing response.
      properties:
        archived_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Archived At
        created_at:
          format: date-time
          title: Created At
          type: string
        created_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Created By User Id
        created_by_user_name:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Display label for the project's creator — the user's ``full_name``
            when set, otherwise the email. ``null`` when the creator's user row
            was deleted (the FK column is preserved for audit, but the name can
            no longer be resolved). Populated only on the list endpoint (a
            single batched ``SELECT FROM users WHERE id IN (...)`` over the
            page); single-project responses default to null. The FE list table
            renders this in a Created-by column.
          title: Created By User Name
        default_branch:
          anyOf:
            - type: string
            - type: 'null'
          title: Default Branch
        description:
          anyOf:
            - type: string
            - type: 'null'
          title: Description
        git_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Git Url
        has_git_credential:
          default: false
          title: Has Git Credential
          type: boolean
        id:
          format: uuid
          title: Id
          type: string
        last_scan_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            Timestamp of the most recent scan *attempt* (any status). `null`
            when the project has never been scanned. Populated only on the list
            endpoint; defaults to null on single-project responses.
          title: Last Scan At
        latest_scan_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Latest Scan Id
        latest_scan_status:
          anyOf:
            - enum:
                - queued
                - running
                - succeeded
                - failed
                - cancelled
              type: string
            - type: 'null'
          description: >-
            Status of the project's latest scan *attempt* (the scan pointed at
            by `latest_scan_id`): queued|running|succeeded|failed|cancelled.
            `null` when the project has never been scanned — the UI renders an
            'Idle' badge. This tracks the attempt timeline, NOT the current SCA
            posture (a failed latest attempt still shows `failed` here while
            `severity_summary` reflects the last succeeded scan).
          title: Latest Scan Status
        license_category_summary:
          anyOf:
            - $ref: '#/components/schemas/LicenseCategorySummary'
            - type: 'null'
          description: >-
            License-category component counts from the project's latest
            *succeeded* scan (same anchor as severity_summary). `null` when the
            project has no succeeded scan. Drives the Projects-page by-project
            license distribution card + segment-click filter; the FE collapses
            each project's counts to its worst non-zero bucket. Populated only
            on the list endpoint (defaults to null on single-project responses).
        name:
          title: Name
          type: string
        release_count:
          default: 0
          description: >-
            Count of succeeded scans (the 'release' model: every succeeded scan
            IS a release snapshot). Populated only on the list endpoint;
            defaults to 0 on single-project responses.
          minimum: 0
          title: Release Count
          type: integer
        scan_count:
          default: 0
          description: >-
            Total scan attempts for the project (any status). Populated only on
            the list endpoint; defaults to 0 on single-project responses.
          minimum: 0
          title: Scan Count
          type: integer
        severity_summary:
          anyOf:
            - $ref: '#/components/schemas/SeveritySummary'
            - type: 'null'
          description: >-
            Vulnerability-severity component counts from the project's latest
            *succeeded* scan (the same anchor the overview / build-gate use).
            `null` when the project has no succeeded scan. Drives the per-row
            risk indicator. Note this can be non-null even when
            `latest_scan_status` is 'failed' — the last *attempt* failed but an
            earlier scan succeeded and its findings remain the current posture.
        slug:
          title: Slug
          type: string
        team_id:
          format: uuid
          title: Team Id
          type: string
        updated_at:
          format: date-time
          title: Updated At
          type: string
        visibility:
          enum:
            - team
            - organization
          title: Visibility
          type: string
      required:
        - id
        - team_id
        - name
        - slug
        - description
        - git_url
        - default_branch
        - visibility
        - archived_at
        - created_by_user_id
        - latest_scan_id
        - created_at
        - updated_at
      title: ProjectPublic
      type: object
    ProjectUpdate:
      additionalProperties: false
      description: >-
        Inbound payload for PATCH /v1/projects/{project_id}.


        `team_id` and `slug` are intentionally NOT updatable: changing the team

        would require re-scoping every audit log, scan, and finding; changing
        the

        slug would invalidate webhook URLs and CLI bookmarks. If the product
        ever

        needs slug rename, model it as a separate `POST
        /v1/projects/{id}:rename`

        operation that does the rewrite in one transaction.
      properties:
        clear_git_credential:
          default: false
          description: >-
            Set true to remove a stored git credential (column → NULL). Cannot
            be combined with a non-empty `git_credential`.
          title: Clear Git Credential
          type: boolean
        default_branch:
          anyOf:
            - maxLength: 255
              type: string
            - type: 'null'
          title: Default Branch
        description:
          anyOf:
            - maxLength: 4000
              type: string
            - type: 'null'
          title: Description
        git_credential:
          anyOf:
            - maxLength: 8192
              type: string
            - type: 'null'
          description: >-
            Write-only plaintext git credential (PAT / deploy token) for cloning
            a private repo. Encrypted at rest; NEVER returned in any response.
            Provide a non-empty value to set/rotate it. Omit to leave it
            unchanged. Use `clear_git_credential: true` to remove it.
          title: Git Credential
        git_url:
          anyOf:
            - maxLength: 2048
              type: string
            - type: 'null'
          title: Git Url
        name:
          anyOf:
            - maxLength: 255
              minLength: 1
              type: string
            - type: 'null'
          title: Name
        visibility:
          anyOf:
            - enum:
                - team
                - organization
              type: string
            - type: 'null'
          title: Visibility
      title: ProjectUpdate
      type: object
    RecentScan:
      description: One row in the dashboard's recent-scans feed (newest first).
      properties:
        finished_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            When the scan reached a terminal state (``scans.completed_at``).
            ``null`` for a scan that is still queued or running.
          title: Finished At
        kind:
          description: Scan kind enum value (source|container).
          title: Kind
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
        project_name:
          title: Project Name
          type: string
        release:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Feature #18 Part A — the optional release/version label the scan was
            triggered with (e.g. "v1.2.3"), read from
            ``scans.metadata.release``. ``null`` when the scan carried no
            release label.
          title: Release
        scan_id:
          format: uuid
          title: Scan Id
          type: string
        status:
          description: Scan status enum value (queued|running|succeeded|failed|cancelled).
          title: Status
          type: string
      required:
        - scan_id
        - project_id
        - project_name
        - status
        - kind
      title: RecentScan
      type: object
    RegisterRequest:
      description: Inbound payload for POST /auth/register.
      properties:
        email:
          format: email
          title: Email
          type: string
        full_name:
          anyOf:
            - maxLength: 255
              type: string
            - type: 'null'
          title: Full Name
        password:
          description: At least 8 characters (NIST 800-63B minimum), not a common password.
          maxLength: 256
          minLength: 8
          title: Password
          type: string
      required:
        - email
        - password
      title: RegisterRequest
      type: object
    ReleaseListResponse:
      description: Paginated list of a project's release snapshots (succeeded scans).
      example:
        items:
          - component_count: 42
            created_at: '2026-05-22T10:00:00Z'
            gate_status: fail
            release: v1.2.3
            risk_score: 92.9
            scan_id: 7822b62d-9156-423d-9df6-5e51f546fbe8
            severity_summary:
              critical: 10
              high: 4
              low: 1
              medium: 2
        page: 1
        size: 20
        total: 1
      properties:
        items:
          items:
            $ref: '#/components/schemas/ReleaseSnapshot'
          title: Items
          type: array
        page:
          minimum: 1
          title: Page
          type: integer
        size:
          maximum: 100
          minimum: 1
          title: Size
          type: integer
        total:
          description: Total succeeded scans for the project (pre-pagination).
          minimum: 0
          title: Total
          type: integer
      required:
        - total
        - page
        - size
      title: ReleaseListResponse
      type: object
    ReleaseSeveritySummary:
      description: >-
        Per-snapshot vulnerability-severity *component* counts.


        Counts are the number of component_versions whose WORST open CVE finding

        lands in each bucket, computed over THAT scan (``WHERE scan_id =
        <row>``) —

        the same worst-per-component bucketing the project-list badge /
        dashboard /

        overview use, so the Releases table can never disagree with the Overview
        tab

        for the same pinned scan. Only the four risk-bearing buckets are
        surfaced

        (``info`` / ``none`` are not actionable on a release row). All four keys
        are

        always present (zero when the snapshot carried no findings in that
        bucket).
      properties:
        critical:
          default: 0
          minimum: 0
          title: Critical
          type: integer
        high:
          default: 0
          minimum: 0
          title: High
          type: integer
        low:
          default: 0
          minimum: 0
          title: Low
          type: integer
        medium:
          default: 0
          minimum: 0
          title: Medium
          type: integer
      title: ReleaseSeveritySummary
      type: object
    ReleaseSnapshot:
      description: One succeeded scan, summarised as a release snapshot row.
      properties:
        component_count:
          description: Distinct component_versions observed in this snapshot.
          minimum: 0
          title: Component Count
          type: integer
        created_at:
          description: >-
            When this scan was created (snapshots are ordered newest-first by
            this).
          format: date-time
          title: Created At
          type: string
        gate_status:
          anyOf:
            - enum:
                - pass
                - fail
              type: string
            - type: 'null'
          description: >-
            The build-gate verdict for THIS snapshot (same evaluation the
            gate-result endpoint runs, pinned to this scan): 'pass' / 'fail', or
            null if not evaluable.
          title: Gate Status
        release:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional release/version label from the scan's ``metadata.release``
            (e.g. 'v1.2.3'). Non-unique and often absent (null).
          title: Release
        risk_score:
          anyOf:
            - maximum: 100
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            Overall risk 0–100 for this snapshot = max(security, license) axis,
            using the SAME non-saturating scorer as the Overview tab
            (services.risk_score). Null only if the snapshot is not aggregable
            (should not occur for a succeeded scan).
          title: Risk Score
        scan_id:
          description: >-
            The succeeded scan that IS this snapshot. Use as the ``?scan_id=``
            anchor on the detail endpoints to pin this release.
          format: uuid
          title: Scan Id
          type: string
        severity_summary:
          $ref: '#/components/schemas/ReleaseSeveritySummary'
          description: Worst-per-component vuln-severity counts for this snapshot.
      required:
        - scan_id
        - created_at
        - severity_summary
        - component_count
      title: ReleaseSnapshot
      type: object
    RemediationPackageChangeOut:
      description: >-
        One package bump recorded on the PR (for audit / human review).


        ``populate_by_name`` lets the service build this from keyword args

        (``from_version`` / ``to_version``) while the wire / JSONB shape uses
        the

        natural ``from`` / ``to`` keys via the field aliases (``from`` is a
        Python

        keyword, hence the suffix on the attribute name).
      examples:
        - from: 4.17.20
          package: lodash
          to: 4.17.21
      properties:
        from:
          anyOf:
            - type: string
            - type: 'null'
          description: The version the scan saw (advisory; may be null).
          title: From
        package:
          description: The npm package name (scoped names kept).
          title: Package
          type: string
        to:
          description: The minimum-safe upgrade target the PR applies.
          title: To
          type: string
      required:
        - package
        - to
      title: RemediationPackageChangeOut
      type: object
    RemediationPullRequestList:
      description: A page of remediation-PR records for a project (newest first).
      examples:
        - items:
            - base_branch: main
              created_at: '2026-05-25T12:00:00Z'
              ecosystem: npm
              head_branch: trustedoss/remediation-1a2b3c4d
              id: 9c2b1a0f-7e11-4a1e-9c3d-5b8f1c2e0c2a
              package_changes:
                - from: 4.17.20
                  package: lodash
                  to: 4.17.21
              pr_number: 42
              pr_url: https://github.com/acme/widget/pull/42
              project_id: 5b8f1c2e-0c2a-4a1e-9c3d-9c2b1a0f7e11
              repository_full_name: acme/widget
              status: open
              updated_at: '2026-05-25T12:00:01Z'
          total: 1
      properties:
        items:
          description: The remediation-PR records.
          items:
            $ref: '#/components/schemas/RemediationPullRequestOut'
          title: Items
          type: array
        total:
          description: Total records for the project.
          title: Total
          type: integer
      required:
        - total
      title: RemediationPullRequestList
      type: object
    RemediationPullRequestOut:
      description: The persisted remediation-PR record returned to the UI.
      examples:
        - base_branch: main
          created_at: '2026-05-25T12:00:00Z'
          ecosystem: npm
          head_branch: trustedoss/remediation-1a2b3c4d
          id: 9c2b1a0f-7e11-4a1e-9c3d-5b8f1c2e0c2a
          package_changes:
            - from: 4.17.20
              package: lodash
              to: 4.17.21
          pr_number: 42
          pr_url: https://github.com/acme/widget/pull/42
          project_id: 5b8f1c2e-0c2a-4a1e-9c3d-9c2b1a0f7e11
          repository_full_name: acme/widget
          status: open
          updated_at: '2026-05-25T12:00:01Z'
      properties:
        base_branch:
          description: The repo default branch the PR targets.
          title: Base Branch
          type: string
        created_at:
          description: When the record was created.
          format: date-time
          title: Created At
          type: string
        ecosystem:
          description: Always 'npm' for this endpoint.
          title: Ecosystem
          type: string
        head_branch:
          description: The branch the portal created for the bump.
          title: Head Branch
          type: string
        id:
          description: The remediation-PR record id.
          format: uuid
          title: Id
          type: string
        package_changes:
          description: The package bumps the PR applies.
          items:
            $ref: '#/components/schemas/RemediationPackageChangeOut'
          title: Package Changes
          type: array
        pr_number:
          anyOf:
            - type: integer
            - type: 'null'
          description: The GitHub PR number (null until opened).
          title: Pr Number
        pr_url:
          anyOf:
            - type: string
            - type: 'null'
          description: The GitHub PR URL (null until opened).
          title: Pr Url
        project_id:
          description: The project the PR remediates.
          format: uuid
          title: Project Id
          type: string
        repository_full_name:
          description: The 'owner/repo' the PR targets (derived from the opt-in link).
          title: Repository Full Name
          type: string
        status:
          description: creating | open | failed | superseded.
          enum:
            - creating
            - open
            - failed
            - superseded
          title: Status
          type: string
        updated_at:
          description: When the record was last updated.
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - project_id
        - ecosystem
        - repository_full_name
        - head_branch
        - base_branch
        - status
        - created_at
        - updated_at
      title: RemediationPullRequestOut
      type: object
    RemediationWarningOut:
      description: A non-fatal note about the dry-run (skip reason / lockfile guidance).
      examples:
        - code: lockfile_regeneration_required
          detail: >-
            package.json was edited; run `npm install` to regenerate
            package-lock.json
      properties:
        code:
          description: Machine-readable warning code.
          title: Code
          type: string
        detail:
          description: Human-readable explanation.
          title: Detail
          type: string
        package:
          anyOf:
            - type: string
            - type: 'null'
          description: The package the warning concerns, if any.
          title: Package
      required:
        - code
        - detail
      title: RemediationWarningOut
      type: object
    ReportDownloadEntry:
      description: |-
        One row in the Reports history list response.

        Frozen contract — the SPA's Reports tab depends on every field name. Add
        new optional fields as nullable; do not rename. ``size_bytes`` is None
        when the emit happened before the bytes were materialised (e.g. a future
        streaming surface) — current emit sites always know the size.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        format:
          description: >-
            Free token (cyclonedx-json / spdx-tv / pdf / text / cdx-vex / …).
            Not an enum because new export formats appear on the timescale of
            feature work; the writer is the single source of values.
          title: Format
          type: string
        id:
          format: uuid
          title: Id
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
        report_type:
          enum:
            - notice
            - sbom
            - vuln_pdf
            - vex_export
          title: Report Type
          type: string
        scan_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          description: >-
            The scan that produced this artefact. NULL for ``vex_export`` rows
            by design (VEX summarises the project's current finding state and is
            not scan-bound), and NULL when the originating scan was later pruned
            (the FK is ``ON DELETE SET NULL``).
          title: Scan Id
        size_bytes:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: Body length in bytes when known at emit time, else NULL.
          title: Size Bytes
        team_id:
          description: >-
            Denormalised tenant pointer — mirrored from the parent project at
            emit time so admin / team-wide queries do not require a join.
          format: uuid
          title: Team Id
          type: string
        user:
          anyOf:
            - $ref: '#/components/schemas/ReportDownloadUserSummary'
            - type: 'null'
          description: >-
            The actor who triggered the download. NULL when the user account was
            deleted (FK is ``ON DELETE SET NULL``) — the history fact 'someone
            on this team got this file' survives the actor.
      required:
        - id
        - project_id
        - team_id
        - report_type
        - format
        - created_at
      title: ReportDownloadEntry
      type: object
    ReportDownloadUserSummary:
      description: >-
        Small per-row actor summary for the history list response.


        Mirrors the ``actor_email`` / ``actor_user_id`` shape used by the admin

        audit log search response (``schemas.admin_ops.AuditLogItem``) but
        packed

        into one nested object so the frontend can render the cell with a single

        truthiness check.
      properties:
        email:
          title: Email
          type: string
        id:
          format: uuid
          title: Id
          type: string
      required:
        - id
        - email
      title: ReportDownloadUserSummary
      type: object
    ReportHistoryResponse:
      description: >-
        Paginated list response for the project Reports tab.


        Pagination mirrors ``NotificationListResponse`` and ``AuditLogListPage``
        so

        a single virtualised-list harness drives all three surfaces.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ReportDownloadEntry'
          title: Items
          type: array
        page:
          minimum: 1
          title: Page
          type: integer
        page_size:
          minimum: 1
          title: Page Size
          type: integer
        total:
          minimum: 0
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - page_size
      title: ReportHistoryResponse
      type: object
    ResetPasswordRequest:
      description: |-
        Inbound payload for POST /auth/reset-password.

        The new password reuses the registration policy (≥ 8 chars / NIST
        800-63B minimum). The token is a URL-safe string up to ~64 chars
        (``secrets.token_urlsafe(32)`` produces ~43 chars; we cap at 256 for
        defence in depth against pathological inputs).
      properties:
        new_password:
          description: At least 8 characters (NIST 800-63B minimum), not a common password.
          maxLength: 256
          minLength: 8
          title: New Password
          type: string
        token:
          maxLength: 256
          minLength: 8
          title: Token
          type: string
      required:
        - token
        - new_password
      title: ResetPasswordRequest
      type: object
    ScanCreate:
      additionalProperties: false
      description: |-
        Inbound payload for POST /v1/projects/{project_id}/scans.

        `kind` selects the scan pipeline (source = cdxgen + ORT + DT;
        container = Trivy). All scan inputs (git_ref, image_ref, ORT options)
        travel inside `metadata` so the schema does not have to grow a field
        every time the pipeline learns a new knob.
      properties:
        kind:
          default: source
          enum:
            - source
            - container
            - sbom
          title: Kind
          type: string
        metadata:
          additionalProperties: true
          title: Metadata
          type: object
      title: ScanCreate
      type: object
    ScanListResponse:
      description: Page of scans for a project.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ScanPublic'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        size:
          title: Size
          type: integer
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - page
        - size
      title: ScanListResponse
      type: object
    ScanPublic:
      description: Outbound shape for every scan-bearing response.
      properties:
        celery_task_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Celery Task Id
        completed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Completed At
        created_at:
          format: date-time
          title: Created At
          type: string
        current_step:
          anyOf:
            - type: string
            - type: 'null'
          title: Current Step
        error_message:
          anyOf:
            - type: string
            - type: 'null'
          title: Error Message
        id:
          format: uuid
          title: Id
          type: string
        kind:
          enum:
            - source
            - container
            - sbom
          title: Kind
          type: string
        metadata:
          additionalProperties: true
          title: Metadata
          type: object
        progress_percent:
          title: Progress Percent
          type: integer
        project_id:
          format: uuid
          title: Project Id
          type: string
        project_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Project Name
        project_slug:
          anyOf:
            - type: string
            - type: 'null'
          title: Project Slug
        ref:
          anyOf:
            - type: string
            - type: 'null'
          title: Ref
        release:
          anyOf:
            - type: string
            - type: 'null'
          title: Release
        requested_by_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Requested By User Id
        started_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Started At
        status:
          enum:
            - queued
            - running
            - succeeded
            - failed
            - cancelled
          title: Status
          type: string
        superseded_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Superseded At
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - project_id
        - kind
        - status
        - progress_percent
        - current_step
        - started_at
        - completed_at
        - error_message
        - requested_by_user_id
        - celery_task_id
        - created_at
        - updated_at
      title: ScanPublic
      type: object
    ScanStatusCounts:
      description: >-
        Scan counts by lifecycle state over the caller's accessible projects.


        ``cancelled`` scans are deliberately excluded — they are neither
        in-flight

        nor a result, so they do not belong in a portfolio headline.
      properties:
        failed:
          default: 0
          minimum: 0
          title: Failed
          type: integer
        queued:
          default: 0
          minimum: 0
          title: Queued
          type: integer
        running:
          default: 0
          minimum: 0
          title: Running
          type: integer
        succeeded:
          default: 0
          minimum: 0
          title: Succeeded
          type: integer
      title: ScanStatusCounts
      type: object
    ScanSummary:
      description: Compact scan record used by the project overview's recent-scans list.
      properties:
        completed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Completed At
        created_at:
          format: date-time
          title: Created At
          type: string
        id:
          format: uuid
          title: Id
          type: string
        kind:
          title: Kind
          type: string
        progress_percent:
          title: Progress Percent
          type: integer
        release:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional release/version label sourced from
            ``Scan.scan_metadata['release']`` (same field the Versions tab
            renders). ``null`` when the scan was run without a release label.
          title: Release
        started_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Started At
        status:
          title: Status
          type: string
      required:
        - id
        - kind
        - status
        - progress_percent
        - started_at
        - completed_at
        - created_at
      title: ScanSummary
      type: object
    SeveritySummary:
      description: >-
        Per-project vulnerability-severity counts for the project-list risk
        badge.


        Counts are the number of *components* (component_versions) landing in
        each

        severity bucket — the worst CVE finding per component — within the
        project's

        latest **succeeded** scan (resolved via

        ``services.scan_resolution.latest_succeeded_scan_id``, the same anchor
        the

        overview / build-gate use). Only the four risk-bearing buckets are
        surfaced

        (``info`` / ``none`` are not actionable on a list-row indicator). A
        project

        with a succeeded scan but no CVE findings yields all-zero counts; a
        project

        that has never succeeded a scan surfaces ``severity_summary = null``
        instead

        (see ``ProjectPublic.severity_summary``).
      properties:
        critical:
          default: 0
          minimum: 0
          title: Critical
          type: integer
        high:
          default: 0
          minimum: 0
          title: High
          type: integer
        low:
          default: 0
          minimum: 0
          title: Low
          type: integer
        medium:
          default: 0
          minimum: 0
          title: Medium
          type: integer
      title: SeveritySummary
      type: object
    SourceArchiveUploadResponse:
      description: >-
        Outbound shape for POST /v1/projects/{project_id}/source-archive.


        The opaque ``archive_id`` is later echoed into ``ScanCreate.metadata``
        as

        ``{"source_type": "upload", "archive_id": "<id>"}`` to scan the uploaded

        source instead of cloning a git URL.
      example:
        archive_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
      properties:
        archive_id:
          title: Archive Id
          type: string
      required:
        - archive_id
      title: SourceArchiveUploadResponse
      type: object
    SourceFileResponse:
      description: A single source file's bytes (capped) + its per-line license matches.
      examples:
        - byte_size: 1071
          content: |-
            MIT License

            Copyright (c) ...
          encoding: utf-8
          license_matches:
            - end_line: 21
              score: 99.5
              spdx_id: MIT
              start_line: 1
          path: LICENSE
          scan_id: 5b6c0f2e-3a1d-4e8a-9b2c-7d4e1f0a9c33
          truncated: false
      properties:
        byte_size:
          description: Full uncompressed size of the file in bytes.
          minimum: 0
          title: Byte Size
          type: integer
        content:
          anyOf:
            - type: string
            - type: 'null'
          description: Decoded file content (possibly truncated). Null for binary files.
          title: Content
        encoding:
          description: >-
            'utf-8' when ``content`` is decoded text; 'binary' when the file is
            non-text (NUL byte or undecodable) and ``content`` is null.
          enum:
            - utf-8
            - binary
          title: Encoding
          type: string
        license_matches:
          description: >-
            Per-line license matches for THIS path, projected from the folded
            scancode JSON. Empty when the file has no recorded matches.
          items:
            $ref: '#/components/schemas/LicenseMatch'
          title: License Matches
          type: array
        path:
          description: POSIX path of the file relative to the source root.
          title: Path
          type: string
        scan_id:
          description: The scan whose preserved source this file was read from.
          format: uuid
          title: Scan Id
          type: string
        truncated:
          description: >-
            True when ``content`` was capped at the viewer's per-file byte limit
            and does not contain the whole file.
          title: Truncated
          type: boolean
      required:
        - scan_id
        - path
        - byte_size
        - truncated
        - encoding
      title: SourceFileResponse
      type: object
    SourceTreeEntry:
      description: One immediate child (file or directory) of a listed directory.
      examples:
        - byte_size: 1280
          is_dir: false
          license_spdx_ids:
            - MIT
            - Apache-2.0
          name: main.py
          path: src/main.py
      properties:
        byte_size:
          description: >-
            Uncompressed size of the file in bytes. 0 for directories (their
            size is not meaningful in the tar).
          minimum: 0
          title: Byte Size
          type: integer
        is_dir:
          description: True for a directory, False for a regular file.
          title: Is Dir
          type: boolean
        license_spdx_ids:
          description: >-
            Cheap per-file license badge set: the distinct SPDX ids recorded in
            license_findings for this exact source path under the resolved scan.
            Empty for directories and unanalysed files.
          items:
            type: string
          title: License Spdx Ids
          type: array
        name:
          description: Base name of the entry (no path separators).
          title: Name
          type: string
        path:
          description: POSIX path of the entry relative to the source root.
          title: Path
          type: string
      required:
        - name
        - path
        - is_dir
        - byte_size
      title: SourceTreeEntry
      type: object
    SourceTreePage:
      description: A page of immediate children for one directory.
      examples:
        - entries:
            - byte_size: 1280
              is_dir: false
              license_spdx_ids:
                - MIT
              name: main.py
              path: src/main.py
          page: 1
          path: src
          scan_id: 5b6c0f2e-3a1d-4e8a-9b2c-7d4e1f0a9c33
          size: 50
          total: 1
      properties:
        entries:
          description: Immediate children of ``path`` on this page (dirs first).
          items:
            $ref: '#/components/schemas/SourceTreeEntry'
          title: Entries
          type: array
        page:
          description: 1-based page index for this response.
          minimum: 1
          title: Page
          type: integer
        path:
          description: >-
            The directory whose children are listed. Empty string is the source
            root.
          title: Path
          type: string
        scan_id:
          description: The scan whose preserved source this tree was read from.
          format: uuid
          title: Scan Id
          type: string
        size:
          description: Page size used for this response.
          minimum: 1
          title: Size
          type: integer
        total:
          description: Total number of immediate children in ``path`` (all pages).
          minimum: 0
          title: Total
          type: integer
      required:
        - scan_id
        - path
        - entries
        - total
        - page
        - size
      title: SourceTreePage
      type: object
    SystemHealthOut:
      description: Response of ``GET /v1/admin/health`` — aggregated probe set.
      properties:
        components:
          items:
            $ref: '#/components/schemas/HealthComponent'
          title: Components
          type: array
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - components
        - updated_at
      title: SystemHealthOut
      type: object
    TeamMembershipPublic:
      description: Team-membership info embedded in admin user/team responses.
      properties:
        role:
          title: Role
          type: string
        team_id:
          format: uuid
          title: Team Id
          type: string
        team_name:
          title: Team Name
          type: string
      required:
        - team_id
        - team_name
        - role
      title: TeamMembershipPublic
      type: object
    TokenResponse:
      description: Response body for /auth/login and /auth/refresh.
      properties:
        access_token:
          title: Access Token
          type: string
        expires_in:
          title: Expires In
          type: integer
        token_type:
          default: bearer
          title: Token Type
          type: string
      required:
        - access_token
        - expires_in
      title: TokenResponse
      type: object
    TrivyDbStatusOut:
      description: |-
        Response of ``GET /v1/admin/trivy/health`` — W6-#43e.

        Every numeric / temporal field is optional so the "not yet downloaded"
        case can serialise cleanly. The FE keys empty state off ``last_update is
        None`` or ``freshness == "unknown"``.

        Configuration fields (``refresh_interval_hours``, ``cache_dir``,
        ``repository``) are always present — they reflect runtime env, not
        on-disk state, so they survive the no-DB case.
      properties:
        cache_dir:
          description: >-
            Resolved Trivy cache directory the worker reads / writes against.
            Useful for operators verifying air-gapped mounts.
          title: Cache Dir
          type: string
        db_size_bytes:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: Sum of file sizes inside ``cache_dir/db/`` in bytes.
          title: Db Size Bytes
        db_version:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Trivy DB schema descriptor (e.g. ``trivy-db schema v2``). ``null``
            before the first download.
          title: Db Version
        freshness:
          description: >-
            ``fresh`` (< 7d), ``stale`` (7-14d), ``very_stale`` (> 14d), or
            ``unknown`` (DB not yet downloaded).
          enum:
            - fresh
            - stale
            - very_stale
            - unknown
          title: Freshness
          type: string
        last_update:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            ``UpdatedAt`` field of the on-disk
            ``$TRIVY_CACHE_DIR/db/metadata.json``. ``null`` when the DB has not
            been downloaded yet (fresh worker boot).
          title: Last Update
        next_refresh_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            ``last_update + refresh_interval_hours``. ``null`` when
            ``last_update`` is unknown.
          title: Next Refresh At
        refresh_interval_hours:
          description: >-
            Configured cadence between Trivy DB refreshes. Mirrors
            ``TRIVY_DB_REFRESH_HOURS`` (default 168h / weekly).
          minimum: 1
          title: Refresh Interval Hours
          type: integer
        repository:
          description: >-
            OCI repository the worker pulls the DB from. Mirrors
            ``TRIVY_DB_REPOSITORY`` (default ``ghcr.io/aquasecurity/trivy-db``).
          title: Repository
          type: string
        vuln_count:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: >-
            Total advisories tracked. Best-effort: read from ``metadata.json``
            if Trivy populates a count key, otherwise ``null`` — the panel falls
            back to '—'.
          title: Vuln Count
      required:
        - refresh_interval_hours
        - freshness
        - cache_dir
        - repository
      title: TrivyDbStatusOut
      type: object
    UnreadCountOut:
      description: Minimal payload for the bell badge poll.
      properties:
        count:
          title: Count
          type: integer
      required:
        - count
      title: UnreadCountOut
      type: object
    UpgradeRecommendation:
      description: >-
        Minimum-safe-upgrade recommendation for a finding's component (v2.2
        2.2-a3).


        Computed from the per-finding ``fixed_version`` values (2.2-a1) of *all*
        the

        component's open CVEs in the same scan: ``recommended_version`` is their

        semver maximum — the lowest version that resolves every open finding for

        this component at once. ``null`` ``recommended_version`` with a
        ``reason``

        means we declined to recommend (see below); the UI then shows a "no

        recommendation" hint rather than a misleading partial upgrade.


        The ``direct`` / ``max_severity`` / ``max_epss`` fields are *priority

        signals* (not used to compute the version) so the drawer / PR comment
        can

        flag the highest-leverage upgrades — a direct dependency with a
        high-EPSS

        critical CVE is the one to bump first.
      properties:
        direct:
          description: >-
            Priority signal: the component is a direct dependency (the scan's
            ``direct`` flag or graph depth == 1) — actionable in the developer's
            own manifest right now.
          title: Direct
          type: boolean
        finding_count:
          default: 0
          description: Number of the component's open findings the recommendation reflects.
          minimum: 0
          title: Finding Count
          type: integer
        max_epss:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            Priority signal: highest EPSS exploit-probability among the open
            findings.
          example: 0.97123
          title: Max Epss
        max_severity:
          anyOf:
            - enum:
                - critical
                - high
                - medium
                - low
                - info
                - unknown
              type: string
            - type: 'null'
          description: >-
            Priority signal: highest CVE severity among the component's open
            findings.
          title: Max Severity
        reason:
          description: >-
            Why ``recommended_version`` is set or null: 'ok' (computed);
            'no_fix_version' (an open finding has no known fix, so a partial
            upgrade would be misleading); 'unparseable_version' (all fix strings
            were malformed); 'no_open_findings' (the component is fully
            dispositioned).
          enum:
            - ok
            - no_fix_version
            - unparseable_version
            - no_open_findings
          title: Reason
          type: string
        recommended_version:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Semver maximum of the component's open findings' fix versions — the
            lowest version that resolves all of this component's open CVEs in
            the scan. ``null`` when no recommendation could be made (see
            ``reason``).
          example: 2.17.1
          title: Recommended Version
      required:
        - reason
        - direct
      title: UpgradeRecommendation
      type: object
    UserMeResponse:
      description: >-
        ``/auth/me`` — UserPublic plus the caller's team memberships.


        The frontend needs a ``team_id`` to create projects and scope writes.

        The base UserPublic (also returned by /register) stays minimal; the

        membership list lives only on the authenticated /me shape.
        ``memberships``

        is ordered oldest-first so ``memberships[0]`` is a stable default team

        (a self-registered user's auto-created team).
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        email:
          title: Email
          type: string
        full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Full Name
        id:
          format: uuid
          title: Id
          type: string
        is_active:
          title: Is Active
          type: boolean
        is_superuser:
          title: Is Superuser
          type: boolean
        memberships:
          items:
            $ref: '#/components/schemas/MembershipPublic'
          title: Memberships
          type: array
      required:
        - id
        - email
        - is_active
        - is_superuser
        - created_at
      title: UserMeResponse
      type: object
    UserPublic:
      description: Shape returned for every user-bearing response. Never includes secrets.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        email:
          title: Email
          type: string
        full_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Full Name
        id:
          format: uuid
          title: Id
          type: string
        is_active:
          title: Is Active
          type: boolean
        is_superuser:
          title: Is Superuser
          type: boolean
      required:
        - id
        - email
        - is_active
        - is_superuser
        - created_at
      title: UserPublic
      type: object
    VEXImportItemError:
      additionalProperties: false
      description: >-
        One structured skip/error row in the import summary.


        Identifies the offending statement by its VEX coordinates (vulnerability
        id

        + product/affects ref) so the analyst can locate it in the source
        document,

        plus a closed-vocabulary ``reason`` and a human-readable ``detail``.
      properties:
        detail:
          description: Human-readable explanation for the skip/error.
          title: Detail
          type: string
        product:
          anyOf:
            - type: string
            - type: 'null'
          description: Product/affects ref (purl) the statement targeted.
          title: Product
        reason:
          description: Closed-vocabulary reason the statement was not applied.
          enum:
            - unknown_vulnerability
            - unknown_component
            - ambiguous_match
            - unmapped_status
            - illegal_transition
            - already_at_target
            - forbidden_transition
            - malformed_statement
          title: Reason
          type: string
        vulnerability:
          anyOf:
            - type: string
            - type: 'null'
          description: CVE/GHSA/OSV id the statement targeted (None if absent).
          title: Vulnerability
      required:
        - reason
        - detail
      title: VEXImportItemError
      type: object
    VEXImportSummary:
      additionalProperties: false
      description: >-
        Result panel for a VEX import.


        Counts are computed over *findings*, not raw statements: a statement
        that

        matches three component versions contributes three to ``matched`` and up
        to

        three to ``applied``. ``skipped`` + ``applied`` over the matched set,
        plus

        any unmatched statements, are reflected as ``errors`` rows.
      properties:
        applied:
          description: Findings whose status was actually changed.
          minimum: 0
          title: Applied
          type: integer
        errors:
          description: Structured per-statement skip/error reasons.
          items:
            $ref: '#/components/schemas/VEXImportItemError'
          title: Errors
          type: array
        format:
          description: Auto-detected source format of the uploaded document.
          enum:
            - openvex
            - cyclonedx
          title: Format
          type: string
        matched:
          description: Findings that a statement resolved to (before transition).
          minimum: 0
          title: Matched
          type: integer
        skipped:
          description: >-
            Findings/statements deliberately not applied (no-op, illegal
            transition, unknown vuln/purl, ambiguous match, …).
          minimum: 0
          title: Skipped
          type: integer
      required:
        - format
        - matched
        - applied
        - skipped
      title: VEXImportSummary
      type: object
    ValidationError:
      properties:
        ctx:
          title: Context
          type: object
        input:
          title: Input
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          title: Location
          type: array
        msg:
          title: Message
          type: string
        type:
          title: Error Type
          type: string
      required:
        - loc
        - msg
        - type
      title: ValidationError
      type: object
    VexOrigin:
      additionalProperties: true
      description: >-
        Provenance of the VEX document that drove a finding's last transition.


        Populated only when ``analysis_source == 'vex_import'`` (NULL
        otherwise).

        Shape mirrors what ``services/vex_import.py`` persists onto the

        ``vex_origin`` JSONB column: the document @id / serialNumber, author,

        document timestamp, the VEX status the matching statement carried, and
        when

        the import ran. All fields are optional because the two VEX dialects
        carry

        different provenance and producers routinely omit fields.


        Security note: every field here is analyst-/document-supplied text. The

        frontend renders it via React's default text escaping (never

        ``dangerouslySetInnerHTML``); the storage layer already strips control

        chars / NULs. ``extra="allow"`` keeps any future provenance key the
        import

        persists, but the UI only renders the typed fields.
      properties:
        author:
          anyOf:
            - type: string
            - type: 'null'
          description: Document author (OpenVEX author / CycloneDX first tool).
          title: Author
        format:
          anyOf:
            - enum:
                - openvex
                - cyclonedx
              type: string
            - type: 'null'
          description: Source VEX dialect of the consuming document.
          title: Format
        id:
          anyOf:
            - type: string
            - type: 'null'
          description: OpenVEX @id or CycloneDX serialNumber of the document.
          title: Id
        imported_at:
          anyOf:
            - type: string
            - type: 'null'
          description: When the import ran (ISO-8601).
          title: Imported At
        timestamp:
          anyOf:
            - type: string
            - type: 'null'
          description: Document timestamp, as carried by the source document.
          title: Timestamp
        vex_status:
          anyOf:
            - type: string
            - type: 'null'
          description: The raw VEX status the matching statement carried.
          title: Vex Status
      title: VexOrigin
      type: object
    VulnerabilityBulkStatusResponse:
      description: |-
        Envelope for a bulk-transition response.

        Always returned as ``200 OK`` — per-row outcomes carry their own status
        codes. The envelope itself only fails (RFC 7807 422) for shape problems
        (empty list, > BULK_TRANSITION_MAX entries, unknown ``target_status``).
      properties:
        failed:
          minimum: 0
          title: Failed
          type: integer
        results:
          items:
            $ref: '#/components/schemas/VulnerabilityBulkStatusResult'
          title: Results
          type: array
        succeeded:
          minimum: 0
          title: Succeeded
          type: integer
        target_status:
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: Target Status
          type: string
        total:
          description: Distinct finding ids attempted (post-dedupe).
          minimum: 1
          title: Total
          type: integer
      required:
        - target_status
        - total
        - succeeded
        - failed
        - results
      title: VulnerabilityBulkStatusResponse
      type: object
    VulnerabilityBulkStatusResult:
      description: Per-row outcome of one entry in a bulk-transition request.
      properties:
        allowed_to:
          anyOf:
            - items:
                enum:
                  - new
                  - analyzing
                  - exploitable
                  - not_affected
                  - false_positive
                  - suppressed
                  - fixed
                type: string
              type: array
            - type: 'null'
          description: >-
            Legal next states from the row's current status. Populated only for
            422 invalid-transition failures so the UI can hint the user.
          title: Allowed To
        detail:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Human-readable detail for failures, or the no-op note for an
            ``already_at_target`` row. ``null`` for a transitioned row.
          title: Detail
        error:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Short machine-readable OUTCOME code. Failure codes: ``not_found`` /
            ``forbidden`` / ``invalid_transition``. Success codes:
            ``already_at_target`` for an idempotent no-op (M-29 — the row was
            already in the requested status; ``success=true``). ``null`` for a
            row that was actually transitioned.
          title: Error
        finding_id:
          format: uuid
          title: Finding Id
          type: string
        status_code:
          description: >-
            HTTP-style status for this row: 200 (transitioned OR already at
            target — both success), 403 (role insufficient for `→ suppressed`),
            404 (id not found in this project), 422 (transition not allowed by
            the matrix).
          maximum: 599
          minimum: 200
          title: Status Code
          type: integer
        success:
          title: Success
          type: boolean
      required:
        - finding_id
        - success
        - status_code
      title: VulnerabilityBulkStatusResult
      type: object
    VulnerabilityBulkStatusUpdate:
      additionalProperties: false
      description: >-
        Request body for ``POST
        /v1/projects/{id}/vulnerabilities:bulk-transition``.


        Per-row idempotent semantics: the server dedupes ``finding_ids``
        (preserving

        first-occurrence order), then attempts the same transition on each row.

        Per-row failures (invalid transition, cross-team / cross-project id,
        role

        insufficient for ``→ suppressed``) are reported in the response envelope
        —

        successful rows still commit.
      properties:
        finding_ids:
          description: >-
            Finding ids to transition. 1..200 entries; duplicates are
            deduplicated server-side. Ids that do not exist in THIS project
            (cross-project / cross-team / never-existed) are reported as a
            per-row 404 — they do not abort the bulk.
          items:
            format: uuid
            type: string
          maxItems: 200
          minItems: 1
          title: Finding Ids
          type: array
        justification:
          anyOf:
            - maxLength: 4000
              type: string
            - type: 'null'
          description: >-
            Free-form note recorded as ``analysis_justification`` on every
            successfully-transitioned row. Omit to leave each row's existing
            justification untouched.
          title: Justification
        target_status:
          description: >-
            Target status applied to every supplied id. Per-row
            transition-matrix and role checks still run (developer → suppressed
            is blocked per row even in a bulk that contains it).
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: Target Status
          type: string
      required:
        - finding_ids
        - target_status
      title: VulnerabilityBulkStatusUpdate
      type: object
    VulnerabilityDetailResponse:
      description: Full drawer payload for a single vulnerability_findings row.
      properties:
        affected_components:
          items:
            $ref: '#/components/schemas/AffectedComponent'
          title: Affected Components
          type: array
        analysis_justification:
          anyOf:
            - type: string
            - type: 'null'
          title: Analysis Justification
        analysis_source:
          anyOf:
            - enum:
                - manual
                - vex_import
              type: string
            - type: 'null'
          description: >-
            Provenance of the current status: 'vex_import' when an uploaded VEX
            document auto-transitioned this finding, 'manual' (or NULL on legacy
            rows) otherwise. Drives the drawer's provenance badge.
          title: Analysis Source
        analysis_state:
          anyOf:
            - type: string
            - type: 'null'
          title: Analysis State
        analyst_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Analyst User Id
        analyzed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Analyzed At
        created_at:
          format: date-time
          title: Created At
          type: string
        cve_id:
          title: Cve Id
          type: string
        cvss_score:
          anyOf:
            - type: number
            - type: 'null'
          title: Cvss Score
        cvss_vector:
          anyOf:
            - type: string
            - type: 'null'
          title: Cvss Vector
        details:
          anyOf:
            - type: string
            - type: 'null'
          title: Details
        epss_percentile:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            EPSS percentile rank for the CVE, in [0, 1]. ``null`` when no EPSS
            publication exists. Serialized from Numeric(6,5) as a float.
          example: 0.99412
          title: Epss Percentile
        epss_score:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            EPSS exploit-probability for the CVE, in [0, 1]. ``null`` when no
            EPSS publication exists. Serialized from Numeric(6,5) as a float.
          example: 0.97123
          title: Epss Score
        id:
          format: uuid
          title: Id
          type: string
        project_id:
          format: uuid
          title: Project Id
          type: string
        published_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Published At
        reachability_analyzed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            When the reachability signal was last written (UTC, ISO-8601).
            ``null`` until a reachability run touches this finding.
          title: Reachability Analyzed At
        reachability_source:
          anyOf:
            - maxLength: 64
              type: string
            - type: 'null'
          description: >-
            Identifier of the analyser that produced ``reachable``
            ('govulncheck' today). ``null`` when ``reachable`` is ``null``.
          example: govulncheck
          title: Reachability Source
        reachable:
          anyOf:
            - type: boolean
            - type: 'null'
          description: >-
            Tri-state reachability signal (v2.3). ``true`` = the vulnerable
            symbol is reachable on the project's call graph; ``false`` =
            analysed and NOT reachable; ``null`` = not analysed. Drives the
            drawer's reachability badge.
          example: true
          title: Reachable
        references:
          description: Pass-through of vulnerabilities.references JSONB.
          items: {}
          title: References
          type: array
        scan_id:
          format: uuid
          title: Scan Id
          type: string
        severity:
          enum:
            - critical
            - high
            - medium
            - low
            - info
            - unknown
          title: Severity
          type: string
        status:
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: Status
          type: string
        status_history:
          items:
            $ref: '#/components/schemas/VulnerabilityStatusHistoryEntry'
          title: Status History
          type: array
        summary:
          anyOf:
            - type: string
            - type: 'null'
          title: Summary
        updated_at:
          format: date-time
          title: Updated At
          type: string
        upgrade_recommendation:
          anyOf:
            - $ref: '#/components/schemas/UpgradeRecommendation'
            - type: 'null'
          description: >-
            Minimum-safe-upgrade recommendation for this finding's component
            (v2.2 2.2-a3). ``null`` only on legacy responses; otherwise present
            with a ``reason`` even when no concrete version could be
            recommended.
        vex_origin:
          anyOf:
            - $ref: '#/components/schemas/VexOrigin'
            - type: 'null'
          description: >-
            Provenance of the consuming VEX document when analysis_source ==
            'vex_import' (NULL otherwise).
      required:
        - id
        - project_id
        - scan_id
        - cve_id
        - severity
        - status
        - created_at
        - updated_at
      title: VulnerabilityDetailResponse
      type: object
    VulnerabilityListItem:
      description: One row in the Vulnerabilities tab table.
      properties:
        affected_component_count:
          description: >-
            Distinct component_versions in the same scan affected by this CVE. 1
            in the common case; >1 only when several distinct packages bundle
            the same vulnerable code.
          minimum: 1
          title: Affected Component Count
          type: integer
        affected_component_license:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            SPDX id of the worst-category license attached to this row's
            component_version in this scan (e.g. ``GPL-3.0-only`` outranks an
            OR'd ``MIT`` because the policy axis picks the strongest claim).
            Identical aggregation rule as
            ``affected_component_license_category`` so the SPDX string and the
            category badge always agree. ``null`` when the cv has no license
            finding in this scan (paired with
            ``affected_component_license_category='unknown'``) or when the
            worst-rank license has no SPDX id (a ``LicenseRef-*`` custom license
            — we never invent an SPDX string for those).
          example: MIT
          title: Affected Component License
        affected_component_license_category:
          anyOf:
            - enum:
                - forbidden
                - conditional
                - allowed
                - unknown
              type: string
            - type: 'null'
          description: >-
            Policy category of the SPDX id reported in
            ``affected_component_license``. Mirrors the longstanding
            ``component_license_category`` field for the same row (kept for
            back-compat); both share the same ``_license_rank_case`` so the
            components tab, the vulnerabilities tab's license badge, and the
            SPDX-paired category never disagree. ``null`` only when
            ``affected_component_license`` is also ``null``.
          example: allowed
          title: Affected Component License Category
        affected_component_name:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Name of the component_version THIS finding row is FK-pinned to
            (``component_versions.component_id`` → ``components.name``). The row
            already represents a single (cv × vulnerability) pairing — this is
            just the human-readable label for that cv, NOT an aggregate. When
            ``affected_component_count > 1`` the OTHER affected cvs surface in
            the drawer's ``affected_components`` list; the list row deliberately
            shows only the pinned cv so the wire stays n+1-free. ``null`` only
            on legacy rows where the FK target was since deleted (the CASCADE
            rule means this is effectively unreachable in practice).
          example: lodash
          title: Affected Component Name
        affected_component_version:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Version string of the component_version this finding row is pinned
            to (``component_versions.version``). Same single-cv rationale as
            ``affected_component_name``. ``null`` paired with a ``null`` name
            when the underlying cv is missing.
          example: 4.17.20
          title: Affected Component Version
        analysis_source:
          anyOf:
            - enum:
                - manual
                - vex_import
              type: string
            - type: 'null'
          description: >-
            Provenance of the row's current status: 'vex_import' when an
            uploaded VEX document auto-transitioned it, 'manual' (or NULL on
            legacy rows) otherwise. Lets the list render a 'VEX' provenance
            marker and back the 'suppressed via VEX' filter.
          title: Analysis Source
        component_license_category:
          default: unknown
          description: >-
            W2 #33 — BD-style 'License risk axis' alongside severity/EPSS.
            Worst-category license attached to THIS row's component_version in
            this scan (mirrors the components-tab classification, sharing
            _license_rank_case so the two tabs never disagree). ``unknown`` when
            the cv has no license finding in this scan; never invented. Kept
            alongside ``affected_component_license_category`` for back-compat —
            both fields encode the same value but ``component_license_category``
            is non-null (defaults to ``unknown``) and the new field is
            null-bearing so the UI can tell 'never had a license finding' from a
            stale-cached row.
          enum:
            - forbidden
            - conditional
            - allowed
            - unknown
          example: conditional
          title: Component License Category
          type: string
        cve_id:
          description: External id (CVE-..., GHSA-..., etc.).
          title: Cve Id
          type: string
        cvss_score:
          anyOf:
            - type: number
            - type: 'null'
          title: Cvss Score
        discovered_at:
          description: >-
            When this finding row was first inserted
            (vulnerability_findings.created_at).
          format: date-time
          title: Discovered At
          type: string
        epss_percentile:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            EPSS percentile rank for the CVE, in [0, 1]. ``null`` when no EPSS
            publication exists. Serialized from Numeric(6,5) as a float.
          example: 0.99412
          title: Epss Percentile
        epss_score:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            EPSS exploit-probability for the CVE, in [0, 1]. ``null`` when the
            upstream feed has not published an EPSS score for this CVE.
            Serialized from the Numeric(6,5) column as a float.
          example: 0.97123
          title: Epss Score
        id:
          description: vulnerability_findings.id
          format: uuid
          title: Id
          type: string
        reachability_analyzed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            When the reachability signal was last written (UTC, ISO-8601).
            ``null`` until a reachability run touches this finding — lets the UI
            distinguish a stale signal from a never-analysed one.
          title: Reachability Analyzed At
        reachability_source:
          anyOf:
            - maxLength: 64
              type: string
            - type: 'null'
          description: >-
            Identifier of the analyser that produced ``reachable``
            ('govulncheck' today). ``null`` when ``reachable`` is ``null``.
          example: govulncheck
          title: Reachability Source
        reachable:
          anyOf:
            - type: boolean
            - type: 'null'
          description: >-
            Tri-state reachability signal (v2.3). ``true`` = the vulnerable
            symbol is reachable on the project's call graph; ``false`` = an
            analyser ran and concluded it is NOT reachable; ``null`` = not
            analysed (no reachability run, or the package was out of the
            analyser's language scope). Drives the list's reachability badge and
            the ``?reachable=`` filter / ``sort=reachable`` ranking.
          example: true
          title: Reachable
        severity:
          enum:
            - critical
            - high
            - medium
            - low
            - info
            - unknown
          title: Severity
          type: string
        status:
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: Status
          type: string
        summary:
          anyOf:
            - type: string
            - type: 'null'
          title: Summary
        updated_at:
          format: date-time
          title: Updated At
          type: string
      required:
        - id
        - cve_id
        - severity
        - status
        - affected_component_count
        - discovered_at
        - updated_at
      title: VulnerabilityListItem
      type: object
    VulnerabilityListResponse:
      description: Page of CVE findings for a project's latest scan.
      properties:
        items:
          items:
            $ref: '#/components/schemas/VulnerabilityListItem'
          title: Items
          type: array
        limit:
          title: Limit
          type: integer
        offset:
          title: Offset
          type: integer
        severity_distribution:
          additionalProperties:
            type: integer
          description: >-
            Count of findings per severity bucket for the resolved snapshot,
            ignoring list filters. Keys are a subset of {critical, high, medium,
            low, info, unknown}. The Vulnerabilities tab renders this as a
            summary card above the paginated rows so the user can see the full
            distribution while the rows below reflect the active filter.
          title: Severity Distribution
          type: object
        total:
          title: Total
          type: integer
      required:
        - items
        - total
        - limit
        - offset
      title: VulnerabilityListResponse
      type: object
    VulnerabilityRef:
      description: Compact CVE reference attached to a component detail.
      properties:
        cve_id:
          description: DT/NVD external id (e.g. CVE-2024-1234, GHSA-...).
          title: Cve Id
          type: string
        cvss:
          anyOf:
            - type: number
            - type: 'null'
          title: Cvss
        description:
          anyOf:
            - type: string
            - type: 'null'
          title: Description
        epss_percentile:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: EPSS percentile rank for the CVE, in [0, 1]. ``null`` when unset.
          title: Epss Percentile
        epss_score:
          anyOf:
            - maximum: 1
              minimum: 0
              type: number
            - type: 'null'
          description: >-
            EPSS exploit-probability for the CVE, in [0, 1]. ``null`` when no
            EPSS publication exists for this CVE.
          title: Epss Score
        fixed_version:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Version that remediates this CVE for this component, when the scan
            pipeline could determine one from DT findings (v2.2). ``null`` when
            DT reported no fix version, or for findings scanned before v2.2.
          title: Fixed Version
        severity:
          title: Severity
          type: string
        title:
          title: Title
          type: string
      required:
        - cve_id
        - severity
        - title
      title: VulnerabilityRef
      type: object
    VulnerabilitySeverityCounts:
      description: >-
        Component findings by worst severity, over each project's latest
        succeeded scan.


        A finding's persisted severity of ``unknown`` is folded into ``info``.
      properties:
        critical:
          default: 0
          minimum: 0
          title: Critical
          type: integer
        high:
          default: 0
          minimum: 0
          title: High
          type: integer
        info:
          default: 0
          minimum: 0
          title: Info
          type: integer
        low:
          default: 0
          minimum: 0
          title: Low
          type: integer
        medium:
          default: 0
          minimum: 0
          title: Medium
          type: integer
      title: VulnerabilitySeverityCounts
      type: object
    VulnerabilityStatusHistoryEntry:
      description: >-
        One transition in the finding's status history (derived from
        audit_logs).
      properties:
        action:
          description: Always 'update' for status transitions.
          title: Action
          type: string
        actor_user_id:
          anyOf:
            - format: uuid
              type: string
            - type: 'null'
          title: Actor User Id
        created_at:
          format: date-time
          title: Created At
          type: string
        new_status:
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: New Status
          type: string
        previous_status:
          anyOf:
            - enum:
                - new
                - analyzing
                - exploitable
                - not_affected
                - false_positive
                - suppressed
                - fixed
              type: string
            - type: 'null'
          description: >-
            Status the row carried *before* this audit row. Derived by walking
            audit_logs in chronological order; the first entry's previous_status
            is null because the row started at the server default ('new').
          title: Previous Status
        request_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Id
      required:
        - action
        - new_status
        - created_at
      title: VulnerabilityStatusHistoryEntry
      type: object
    VulnerabilityStatusUpdate:
      additionalProperties: false
      description: PATCH body for /vulnerability_findings/{id}/status.
      properties:
        if_match:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: >-
            Optional optimistic-concurrency token. When supplied, the server
            compares this against the current row's updated_at. Mismatch → 409
            Conflict (RFC 7807). When omitted, the update proceeds without lock
            (best-effort).
          title: If Match
        justification:
          anyOf:
            - maxLength: 4000
              type: string
            - type: 'null'
          description: >-
            Free-form note recorded as analysis_justification. Required by the
            UI for VEX-significant transitions but enforced softly: the API
            accepts an empty justification (regulators often re-trigger the
            transition once with the note attached).
          title: Justification
        status:
          description: Target status.
          enum:
            - new
            - analyzing
            - exploitable
            - not_affected
            - false_positive
            - suppressed
            - fixed
          title: Status
          type: string
      required:
        - status
      title: VulnerabilityStatusUpdate
      type: object
info:
  description: >-
    Open-source self-hosted SCA portal — CVE, license compliance, and SBOM
    management with EPSS prioritization, VEX consumption, CI build gating, and
    Trivy-backed CVE matching with weekly DB refresh + automatic re-matching on
    new vulnerability data.
  title: TRUSCA API
  version: 2.2.0-dev
openapi: 3.1.0
paths:
  /auth/forgot-password:
    post:
      description: |-
        Public — no authentication required, but limited per
        ``PASSWORD_RESET_RATE_LIMIT`` (5/min/IP by default).

        Body shape: ``{"email": "<address>"}``. Always returns 204 + empty body
        (CWE-204). When the address matches a registered user we additionally
        enqueue an email via Celery. When the per-email cooldown is active we
        set ``Retry-After`` to the configured cooldown and STILL return 204.
      operationId: forgot_password_auth_forgot_password_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForgotPasswordRequest'
        required: true
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Request a password reset link (public, rate limited)
      tags:
        - auth
  /auth/login:
    post:
      description: >-
        Public — no authentication required, but limited to 5
        attempts/minute/IP.


        On success: 200 + access_token in the body, refresh as HttpOnly cookie.

        On bad credentials: 401 problem+json.
      operationId: login_auth_login_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Login (public, rate limited)
      tags:
        - auth
  /auth/logout:
    post:
      description: |-
        Revoke the refresh cookie. Idempotent — always returns 204 even if the
        cookie is absent or already revoked.
      operationId: logout_auth_logout_post
      parameters:
        - in: cookie
          name: refresh_token
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Refresh Token
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Logout (revoke refresh cookie)
      tags:
        - auth
  /auth/me:
    get:
      description: >-
        Authenticated. UserPublic + the caller's team memberships.


        The frontend reads ``memberships`` to resolve a ``team_id`` for project

        creation / write scoping. Ordered oldest-first so ``memberships[0]`` is
        a

        stable default team.
      operationId: me_auth_me_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserMeResponse'
          description: Successful Response
      summary: Return the currently authenticated user and their memberships
      tags:
        - auth
  /auth/oauth/providers:
    get:
      description: |-
        Public — no authentication required (explicit exception to CLAUDE.md
        core rule #12: the consumer is the anonymous /login page, which must
        decide which OAuth sign-in buttons to render BEFORE any credential
        exists).

        Always lists every supported provider with a bare ``configured``
        boolean. ``configured`` is ``True`` only when both the client id AND
        client secret are set — the same condition under which
        ``/{provider}/authorize`` actually works (M-15: a half-configured or
        unconfigured provider previously surfaced as a rendered button that
        503'd on click).

        Security: the response carries booleans only — never client ids,
        secrets, or any other configuration detail.
      operationId: oauth_providers_auth_oauth_providers_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthProvidersResponse'
          description: Successful Response
      summary: List OAuth provider availability (public)
      tags:
        - auth
  /auth/oauth/{provider}/authorize:
    get:
      description: |-
        Public — no authentication required.

        302 → provider's authorize URL with a signed state. Returns:
          - 503 + ``oauth_provider_disabled`` Problem Details when the
            provider's client id/secret is not configured.
          - 404 Problem Details when the provider name is unknown (FastAPI's
            Literal[] gate normally catches this; the explicit branch survives
            future enum widening).
          - 403 ``demo_read_only`` Problem Details when the deployment runs in
            read-only live-demo mode (OAuth sign-in is a write; see
            :func:`_demo_read_only_blocked`).
      operationId: oauth_authorize_auth_oauth__provider__authorize_get
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            enum:
              - github
              - google
            title: Provider
            type: string
        - in: query
          name: redirect_after
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Redirect After
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Begin OAuth sign-in (public)
      tags:
        - auth
  /auth/oauth/{provider}/callback:
    get:
      description: >-
        Public — the provider's redirect lands here after consent.


        Success path: 302 → ``redirect_after`` (or configured default) with the

        refresh-token HttpOnly cookie attached.


        Failure path: 302 → configured failure URL with ``?error=<reason>``. The

        provider's own ``?error=access_denied`` (user clicked "Cancel") falls

        through here too — we forward a normalised ``error=oauth_denied``.


        Read-only demo: if ``DEMO_READ_ONLY`` is enabled we 403 BEFORE any token

        exchange or DB write (see :func:`_demo_read_only_blocked`), so the
        callback

        can never create/link a User or Team in the demo.
      operationId: oauth_callback_auth_oauth__provider__callback_get
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            enum:
              - github
              - google
            title: Provider
            type: string
        - in: query
          name: code
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Code
        - in: query
          name: state
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: State
        - in: query
          name: error
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Error
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: OAuth callback (public)
      tags:
        - auth
  /auth/refresh:
    post:
      description: |-
        Public — the refresh cookie is the credential.

        Successful rotation: 200 + new access_token + new refresh cookie.
        Reuse detected (cookie already rotated): 401, entire chain revoked.
      operationId: refresh_auth_refresh_post
      parameters:
        - in: cookie
          name: refresh_token
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Refresh Token
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Rotate refresh token (public; refresh cookie is the credential)
      tags:
        - auth
  /auth/register:
    post:
      description: |-
        Public — no authentication required.

        Returns the new user (without password). 422 for validation errors,
        409 if the email is already registered.
      operationId: register_auth_register_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Register a new user (public)
      tags:
        - auth
  /auth/reset-password:
    post:
      description: |-
        Public — the reset token is the credential.

        On success: 204 + every refresh token for the user is revoked.
        On bad / expired / used token: 422 problem+json.
      operationId: reset_password_auth_reset_password_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
        required: true
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Confirm a password reset using a one-shot token (public)
      tags:
        - auth
  /health:
    get:
      description: >-
        Cheap PURE-LIVENESS probe used by docker-compose / k8s liveness checks.


        PUBLIC / unauthenticated (CLAUDE.md rule #12 explicit exception). This
        proves

        only that the uvicorn process is accepting requests — it does NOT touch
        the

        database and says nothing about schema state. For "is the schema
        migrated and

        safe to serve traffic / start workers", use GET /health/ready
        (api/v1/health.py).


        v2.1 Track B (B5): also surfaces ``demo_read_only`` so the SPA can
        render the

        read-only banner and disable write actions without needing a separate
        build.

        The flag is resolved at request time (CLAUDE.md rule #11), so the same
        image

        behaves correctly whether DEMO_READ_ONLY is set or not.
      operationId: health_health_get
      responses:
        '200':
          content:
            application/json:
              schema:
                additionalProperties: true
                title: Response Health Health Get
                type: object
          description: Successful Response
      summary: Liveness probe — PUBLIC, unauthenticated
      tags:
        - public
  /health/ready:
    get:
      description: >-
        Return 200 when the DB schema matches the Alembic HEAD, else 503.


        PUBLIC: no auth dependency by design (probe endpoint — see module
        docstring

        and CLAUDE.md core rule #12). The check is read-only (a single SELECT on

        ``alembic_version`` plus an in-image read of the script tree).
      operationId: health_ready_health_ready_get
      responses:
        '200':
          content:
            application/json:
              example:
                status: ready
              schema: {}
          description: Schema is at the Alembic HEAD revision.
        '503':
          content:
            application/problem+json:
              example:
                detail: >-
                  database schema is not at the expected Alembic HEAD (expected:
                  0021_abc; current: 0020_def)
                instance: /health/ready
                ready: false
                status: 503
                title: Service Not Ready
                type: urn:trustedoss:problem:schema-not-ready
          description: >-
            Schema is not at HEAD, the alembic_version table is missing, or the
            database is unreachable. RFC 7807 problem+json.
      summary: Readiness probe (schema at Alembic HEAD) — PUBLIC, unauthenticated
      tags:
        - public
  /v1/admin/audit:
    get:
      operationId: search_audit_endpoint_v1_admin_audit_get
      parameters:
        - in: query
          name: actor_user_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Actor User Id
        - in: query
          name: target_table
          required: false
          schema:
            anyOf:
              - enum:
                  - users
                  - teams
                  - memberships
                  - organizations
                  - projects
                  - scans
                  - scan_artifacts
                  - components
                  - component_versions
                  - scan_components
                  - vulnerabilities
                  - vulnerability_findings
                  - licenses
                  - license_findings
                  - obligations
                  - refresh_tokens
                  - password_reset_tokens
                  - license_fetch_cache
                type: string
              - type: 'null'
            title: Target Table
        - in: query
          name: action
          required: false
          schema:
            anyOf:
              - maxLength: 64
                type: string
              - type: 'null'
            title: Action
        - in: query
          name: from
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: From
        - in: query
          name: to
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: To
        - in: query
          name: q
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Q
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditLogListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Search audit log (admin) — paginated, filterable
      tags:
        - admin
        - admin
  /v1/admin/audit/export.csv:
    get:
      operationId: export_audit_csv_endpoint_v1_admin_audit_export_csv_get
      parameters:
        - in: query
          name: actor_user_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Actor User Id
        - in: query
          name: target_table
          required: false
          schema:
            anyOf:
              - enum:
                  - users
                  - teams
                  - memberships
                  - organizations
                  - projects
                  - scans
                  - scan_artifacts
                  - components
                  - component_versions
                  - scan_components
                  - vulnerabilities
                  - vulnerability_findings
                  - licenses
                  - license_findings
                  - obligations
                  - refresh_tokens
                  - password_reset_tokens
                  - license_fetch_cache
                type: string
              - type: 'null'
            title: Target Table
        - in: query
          name: action
          required: false
          schema:
            anyOf:
              - maxLength: 64
                type: string
              - type: 'null'
            title: Action
        - in: query
          name: from
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: From
        - in: query
          name: to
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: To
        - in: query
          name: q
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Q
      responses:
        '200':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Export audit log to CSV (admin) — streaming, capped at 100k rows
      tags:
        - admin
        - admin
  /v1/admin/backup:
    get:
      operationId: list_backups_endpoint_v1_admin_backup_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupListResponse'
          description: Successful Response
      summary: List all backups (admin) — auto + manual, newest first
      tags:
        - admin
        - admin
    post:
      operationId: trigger_backup_endpoint_v1_admin_backup_post
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupTriggerResponse'
          description: Successful Response
      summary: Trigger a manual backup (admin) — enqueues the Celery task
      tags:
        - admin
        - admin
  /v1/admin/backup/restore:
    post:
      operationId: restore_backup_endpoint_v1_admin_backup_restore_post
      parameters:
        - in: header
          name: X-Confirm-Restore
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Confirm-Restore
        - in: header
          name: Content-Length
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Content-Length
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: >-
                #/components/schemas/Body_restore_backup_endpoint_v1_admin_backup_restore_post
        required: true
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupRestoreResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Restore from an uploaded backup archive (admin) — destructive
      tags:
        - admin
        - admin
  /v1/admin/backup/{name}:
    delete:
      operationId: delete_backup_endpoint_v1_admin_backup__name__delete
      parameters:
        - description: Backup directory name.
          in: path
          name: name
          required: true
          schema:
            description: Backup directory name.
            title: Name
            type: string
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete a manual backup (admin) — auto backups are protected
      tags:
        - admin
        - admin
  /v1/admin/backup/{name}/download:
    get:
      operationId: download_backup_endpoint_v1_admin_backup__name__download_get
      parameters:
        - description: Backup directory name.
          in: path
          name: name
          required: true
          schema:
            description: Backup directory name.
            title: Name
            type: string
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download a backup archive (admin) — streams a tar.gz of the directory
      tags:
        - admin
        - admin
  /v1/admin/disk:
    get:
      operationId: get_disk_endpoint_v1_admin_disk_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminDiskOut'
          description: Successful Response
      summary: Disk usage telemetry (admin) — workspace / DT volume / Postgres / Redis
      tags:
        - admin
        - admin
  /v1/admin/health:
    get:
      operationId: get_health_endpoint_v1_admin_health_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemHealthOut'
          description: Successful Response
      summary: System health summary (admin) — postgres / redis / celery / DT / disk
      tags:
        - admin
        - admin
  /v1/admin/scans:
    get:
      operationId: list_scans_endpoint_v1_admin_scans_get
      parameters:
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
        - in: query
          name: status
          required: false
          schema:
            anyOf:
              - enum:
                  - queued
                  - running
                  - succeeded
                  - failed
                  - cancelled
                type: string
              - type: 'null'
            title: Status
        - in: query
          name: kind
          required: false
          schema:
            anyOf:
              - enum:
                  - source
                  - container
                  - sbom
                type: string
              - type: 'null'
            title: Kind
        - in: query
          name: project
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Project
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminScanListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        List scans (admin) — cross-team queue with optional status/kind/project
        filters
      tags:
        - admin
        - admin
  /v1/admin/scans/{scan_id}/cancel:
    post:
      operationId: cancel_scan_endpoint_v1_admin_scans__scan_id__cancel_post
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Force-cancel a scan (admin) — Celery revoke + status='cancelled'
      tags:
        - admin
        - admin
  /v1/admin/teams:
    get:
      operationId: list_teams_endpoint_v1_admin_teams_get
      parameters:
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List teams (admin) — paginated, name search
      tags:
        - admin
        - admin
    post:
      operationId: create_team_endpoint_v1_admin_teams_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminTeamCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create a team (admin)
      tags:
        - admin
        - admin
  /v1/admin/teams/{team_id}:
    delete:
      operationId: delete_team_endpoint_v1_admin_teams__team_id__delete
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete a team (admin) — archives projects, refuses on active scans
      tags:
        - admin
        - admin
    get:
      operationId: get_team_endpoint_v1_admin_teams__team_id__get
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get one team (admin) — detail with members and project count
      tags:
        - admin
        - admin
    patch:
      operationId: update_team_endpoint_v1_admin_teams__team_id__patch
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminTeamUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update a team (admin)
      tags:
        - admin
        - admin
  /v1/admin/teams/{team_id}/members:
    post:
      operationId: add_member_endpoint_v1_admin_teams__team_id__members_post
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminTeamMemberAdd'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Add (or update) a team member (admin)
      tags:
        - admin
        - admin
  /v1/admin/teams/{team_id}/members/{user_id}:
    delete:
      operationId: remove_member_endpoint_v1_admin_teams__team_id__members__user_id__delete
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTeamDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Remove a team member (admin)
      tags:
        - admin
        - admin
  /v1/admin/trivy/health:
    get:
      operationId: get_trivy_db_health_endpoint_v1_admin_trivy_health_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrivyDbStatusOut'
          description: Successful Response
      summary: >-
        Trivy vulnerability DB status (admin) — last_update / freshness /
        vuln_count
      tags:
        - admin
        - admin
  /v1/admin/users:
    get:
      operationId: list_users_endpoint_v1_admin_users_get
      parameters:
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
        - in: query
          name: role
          required: false
          schema:
            anyOf:
              - enum:
                  - super_admin
                  - team_admin
                  - developer
                type: string
              - type: 'null'
            title: Role
        - in: query
          name: active
          required: false
          schema:
            anyOf:
              - type: boolean
              - type: 'null'
            title: Active
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUserListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List users (admin) — paginated, filterable
      tags:
        - admin
        - admin
  /v1/admin/users/{user_id}:
    get:
      operationId: get_user_endpoint_v1_admin_users__user_id__get
      parameters:
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUserDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get one user (admin) — detail with memberships + scan count
      tags:
        - admin
        - admin
  /v1/admin/users/{user_id}/activate:
    patch:
      operationId: activate_user_endpoint_v1_admin_users__user_id__activate_patch
      parameters:
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUserDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Re-activate a user (admin)
      tags:
        - admin
        - admin
  /v1/admin/users/{user_id}/deactivate:
    patch:
      operationId: deactivate_user_endpoint_v1_admin_users__user_id__deactivate_patch
      parameters:
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUserDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Deactivate a user (admin) — revokes refresh tokens
      tags:
        - admin
        - admin
  /v1/admin/users/{user_id}/password-reset:
    post:
      description: >-
        Issues a one-shot reset token (bcrypt-hashed in storage) and returns
        204.


        Phase 6 PR #18 will wire the SMTP / Slack delivery channel. Until then

        the plaintext token is generated, persisted as a hash, audit-logged via

        the listener (which masks the hash to ``***``), and discarded.


        -- Account-enumeration semantics (security-reviewer F5)
        -------------------


        This endpoint returns 404 when ``user_id`` does not exist. That IS an

        enumeration oracle in isolation, but it is acceptable HERE because the

        route is super-admin-gated by ``require_super_admin_or_404`` — any

        caller who can reach this code path is already authorised to read the

        full user list (``GET /v1/admin/users``), so the 404 leaks no

        information they did not already have. The trust boundary is ABOVE this

        endpoint, not at it.


        The Phase 6 PR #18 PUBLIC password-reset flow ("forgot password") MUST

        NOT copy this 404-on-miss pattern. That endpoint is unauthenticated, so

        a 404 vs. 204 split there would let an attacker enumerate registered

        emails (CWE-204 Observable Response Discrepancy). The public flow

        returns a uniform 204 regardless of whether the email exists, with the

        actual reset email sent only when a match is found. See

        ``docs/v2-execution-plan.md`` §3.7 for the Phase 6 contract.
      operationId: password_reset_endpoint_v1_admin_users__user_id__password_reset_post
      parameters:
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Initiate a password reset (admin) — Phase 6 wires email delivery
      tags:
        - admin
        - admin
  /v1/admin/users/{user_id}/role:
    patch:
      operationId: update_user_role_endpoint_v1_admin_users__user_id__role_patch
      parameters:
        - in: path
          name: user_id
          required: true
          schema:
            format: uuid
            title: User Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminUserRoleUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUserDetail'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Change a user's role (admin)
      tags:
        - admin
        - admin
  /v1/api-keys:
    get:
      operationId: list_api_keys_endpoint_v1_api_keys_get
      parameters:
        - in: query
          name: scope
          required: false
          schema:
            anyOf:
              - enum:
                  - org
                  - team
                  - project
                type: string
              - type: 'null'
            title: Scope
        - in: query
          name: team_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Team Id
        - in: query
          name: project_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Project Id
        - in: query
          name: include_revoked
          required: false
          schema:
            default: false
            title: Include Revoked
            type: boolean
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIKeyListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated list of API keys visible to the caller
      tags:
        - api-keys
    post:
      operationId: create_api_key_endpoint_v1_api_keys_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/APIKeyCreateIn'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIKeyCreateOut'
          description: >-
            Key created. ``raw_key`` is the wire bearer string — capture it;
            subsequent reads only return metadata.
        '403':
          description: Caller may not issue at the requested scope.
        '404':
          description: Project not found (when scope='project').
        '422':
          description: >-
            Scope/team_id/project_id combination is invalid (e.g. scope='team'
            with no team_id).
        '503':
          description: >-
            Could not allocate a unique key prefix after retries — transient and
            retryable.
      summary: Issue a new API key (plaintext returned ONCE in raw_key)
      tags:
        - api-keys
  /v1/api-keys/{api_key_id}:
    delete:
      operationId: revoke_api_key_endpoint_v1_api_keys__api_key_id__delete
      parameters:
        - in: path
          name: api_key_id
          required: true
          schema:
            format: uuid
            title: Api Key Id
            type: string
      responses:
        '204':
          description: Key revoked (or was already revoked — idempotent).
        '403':
          description: >-
            Caller can see the key but lacks permission to revoke it (needs to
            be the issuer, a team_admin of the key's team, or super_admin).
        '404':
          description: Key not found, or not visible to the caller.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Revoke (soft-delete) an API key
      tags:
        - api-keys
  /v1/approvals:
    get:
      operationId: list_approvals_endpoint_v1_approvals_get
      parameters:
        - description: >-
            Single status or a comma-separated list of statuses (e.g.
            ``pending,under_review``).
          in: query
          name: status
          required: false
          schema:
            anyOf:
              - pattern: >-
                  ^(pending|under_review|approved|rejected)(,(pending|under_review|approved|rejected))*$
                type: string
              - type: 'null'
            description: >-
              Single status or a comma-separated list of statuses (e.g.
              ``pending,under_review``).
            title: Status
        - in: query
          name: team_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Team Id
        - in: query
          name: requested_by_user_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Requested By User Id
        - in: query
          name: from_dt
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: From Dt
        - in: query
          name: to_dt
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: To Dt
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated list of approval requests (team-scoped; super_admin sees all)
      tags:
        - approvals
    post:
      operationId: create_approval_endpoint_v1_approvals_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalCreateIn'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalOut'
          description: Approval created.
        '404':
          description: Project or component not found, or caller lacks team access.
        '409':
          description: >-
            An open approval already exists for this component + project.
            ``approval_already_open = true`` extension in the Problem Details
            body.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Open a new approval request for a component in a project
      tags:
        - approvals
  /v1/approvals/{approval_id}:
    delete:
      operationId: delete_approval_endpoint_v1_approvals__approval_id__delete
      parameters:
        - in: path
          name: approval_id
          required: true
          schema:
            format: uuid
            title: Approval Id
            type: string
      responses:
        '204':
          description: Approval deleted.
        '404':
          description: Approval not found, or not visible / deletable by the caller.
        '409':
          description: >-
            Approval is in a terminal state (approved / rejected) and cannot be
            deleted. ``approval_terminal_state = true`` extension.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Delete a non-terminal approval (original requester, team_admin, or
        super_admin)
      tags:
        - approvals
    get:
      operationId: get_approval_endpoint_v1_approvals__approval_id__get
      parameters:
        - in: path
          name: approval_id
          required: true
          schema:
            format: uuid
            title: Approval Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalOut'
          description: Approval found. ETag header contains the current version.
          headers:
            ETag:
              description: Current version as quoted string
        '404':
          description: Approval not found, or not visible to the caller.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Single approval detail — includes ETag header for optimistic concurrency
      tags:
        - approvals
  /v1/approvals/{approval_id}/transition:
    patch:
      operationId: transition_approval_endpoint_v1_approvals__approval_id__transition_patch
      parameters:
        - in: path
          name: approval_id
          required: true
          schema:
            format: uuid
            title: Approval Id
            type: string
        - in: header
          name: if-match
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: If-Match
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalTransitionIn'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalOut'
          description: Approval transitioned. Body is the post-commit approval.
        '400':
          description: If-Match header is missing or cannot be parsed.
        '403':
          description: >-
            Caller's role is insufficient for the requested transition
            (under_review / approved / rejected require team_admin or
            super_admin).
        '404':
          description: Approval not found, or not visible to the caller.
        '409':
          description: >-
            Transition is not permitted by the state machine.
            ``approval_invalid_transition = true`` + ``allowed_to`` extension
            lists the valid next states.
        '412':
          description: >-
            If-Match version did not match the row's current version. Re-fetch
            the approval and retry.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Transition an approval's status (requires If-Match header)
      tags:
        - approvals
  /v1/audit:
    get:
      operationId: search_team_audit_endpoint_v1_audit_get
      parameters:
        - in: query
          name: actor_user_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Actor User Id
        - in: query
          name: target_table
          required: false
          schema:
            anyOf:
              - enum:
                  - users
                  - teams
                  - memberships
                  - organizations
                  - projects
                  - scans
                  - scan_artifacts
                  - components
                  - component_versions
                  - scan_components
                  - vulnerabilities
                  - vulnerability_findings
                  - licenses
                  - license_findings
                  - obligations
                  - refresh_tokens
                  - password_reset_tokens
                  - license_fetch_cache
                type: string
              - type: 'null'
            title: Target Table
        - in: query
          name: action
          required: false
          schema:
            anyOf:
              - maxLength: 64
                type: string
              - type: 'null'
            title: Action
        - in: query
          name: from
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: From
        - in: query
          name: to
          required: false
          schema:
            anyOf:
              - format: date-time
                type: string
              - type: 'null'
            title: To
        - in: query
          name: q
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Q
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditLogListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Search audit log scoped to the caller's teams (team_admin+)
      tags:
        - audit
  /v1/components/{component_id}:
    get:
      operationId: get_component_detail_endpoint_v1_components__component_id__get
      parameters:
        - in: path
          name: component_id
          required: true
          schema:
            format: uuid
            title: Component Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComponentDetailResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Component detail (drawer payload). 404 if component is invisible to
        caller.
      tags:
        - components
  /v1/dashboard/summary:
    get:
      description: |-
        Aggregate counts (projects, scans, severities, licenses, approvals) plus
        the 10 most recent scans, scoped to the caller's accessible projects.
      operationId: get_dashboard_summary_endpoint_v1_dashboard_summary_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardSummary'
          description: Successful Response
      summary: Portfolio overview for the caller's accessible projects (auth required)
      tags:
        - dashboard
  /v1/github-app-credentials:
    get:
      operationId: list_credentials_endpoint_v1_github_app_credentials_get
      parameters:
        - in: query
          name: team_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Team Id
        - in: query
          name: include_revoked
          required: false
          schema:
            default: false
            title: Include Revoked
            type: boolean
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GitHubAppCredentialListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated list of GitHub App credentials visible to the caller
      tags:
        - github-app
    post:
      operationId: register_credential_endpoint_v1_github_app_credentials_post
      parameters:
        - description: The team that owns this credential.
          in: query
          name: team_id
          required: true
          schema:
            description: The team that owns this credential.
            format: uuid
            title: Team Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GitHubAppCredentialCreateIn'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GitHubAppCredentialOut'
          description: Credential registered. Private key is never returned.
        '403':
          description: Caller is not a team admin of the target team.
        '409':
          description: A credential for this (team, app_id) already exists.
        '422':
          description: Malformed PEM / app_id / metadata, or unusable key.
      summary: Register a GitHub App credential (private key encrypted at rest)
      tags:
        - github-app
  /v1/github-app-credentials/{credential_id}:
    delete:
      operationId: >-
        revoke_credential_endpoint_v1_github_app_credentials__credential_id__delete
      parameters:
        - in: path
          name: credential_id
          required: true
          schema:
            format: uuid
            title: Credential Id
            type: string
      responses:
        '204':
          description: Revoked (or already revoked — idempotent).
        '403':
          description: Caller is not a team admin of the credential's team.
        '404':
          description: Not found, or not visible to the caller.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Revoke (soft-delete) a GitHub App credential
      tags:
        - github-app
    get:
      operationId: get_credential_endpoint_v1_github_app_credentials__credential_id__get
      parameters:
        - in: path
          name: credential_id
          required: true
          schema:
            format: uuid
            title: Credential Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GitHubAppCredentialOut'
          description: Successful Response
        '404':
          description: Not found, or not visible to the caller.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Fetch one GitHub App credential's metadata
      tags:
        - github-app
  /v1/github-app-credentials/{credential_id}/installations:
    get:
      operationId: >-
        list_installations_endpoint_v1_github_app_credentials__credential_id__installations_get
      parameters:
        - in: path
          name: credential_id
          required: true
          schema:
            format: uuid
            title: Credential Id
            type: string
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GitHubAppInstallationListPage'
          description: Successful Response
        '404':
          description: Credential not found / not visible.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List installations under a credential
      tags:
        - github-app
    post:
      operationId: >-
        link_installation_endpoint_v1_github_app_credentials__credential_id__installations_post
      parameters:
        - in: path
          name: credential_id
          required: true
          schema:
            format: uuid
            title: Credential Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GitHubAppInstallationLinkIn'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GitHubAppInstallationOut'
          description: Installation linked (idempotent on re-link).
        '403':
          description: Caller is not a team admin, or the project belongs to another team.
        '404':
          description: Credential or project not found / not visible.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Link (opt-in) an installation under a credential
      tags:
        - github-app
  /v1/github-app-credentials/{credential_id}/installations/{installation_row_id}:
    delete:
      operationId: >-
        unlink_installation_endpoint_v1_github_app_credentials__credential_id__installations__installation_row_id__delete
      parameters:
        - in: path
          name: credential_id
          required: true
          schema:
            format: uuid
            title: Credential Id
            type: string
        - in: path
          name: installation_row_id
          required: true
          schema:
            format: uuid
            title: Installation Row Id
            type: string
      responses:
        '204':
          description: Unlinked (idempotent — absent link is a no-op).
        '403':
          description: Caller is not a team admin of the credential's team.
        '404':
          description: Credential not found / not visible.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Unlink an installation from a credential
      tags:
        - github-app
  /v1/license-policies:
    get:
      operationId: list_policies_endpoint_v1_license_policies_get
      parameters:
        - in: query
          name: organization_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Organization Id
        - in: query
          name: team_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Team Id
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyListPage'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated list of license policies visible to the caller
      tags:
        - license-policies
  /v1/license-policies/org/{organization_id}:
    get:
      operationId: get_org_policy_endpoint_v1_license_policies_org__organization_id__get
      parameters:
        - in: path
          name: organization_id
          required: true
          schema:
            format: uuid
            title: Organization Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: The org-default policy.
        '404':
          description: No org default, or non-super-admin existence-hide.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Read the org-default license policy (super_admin)
      tags:
        - license-policies
    put:
      operationId: upsert_org_policy_endpoint_v1_license_policies_org__organization_id__put
      parameters:
        - in: path
          name: organization_id
          required: true
          schema:
            format: uuid
            title: Organization Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LicensePolicyUpsertIn'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: Org-default policy created or updated.
        '404':
          description: Not found (non-super-admin existence-hide).
        '409':
          description: An org-default policy already exists (race).
        '422':
          description: Malformed / oversized policy payload.
      summary: Create or update the org-default license policy (super_admin)
      tags:
        - license-policies
  /v1/license-policies/teams/{team_id}:
    delete:
      operationId: delete_team_policy_endpoint_v1_license_policies_teams__team_id__delete
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      responses:
        '204':
          description: Policy deleted; team falls back to org default / static.
        '403':
          description: Caller is not a team_admin of this team.
        '404':
          description: Team or policy not found (existence-hidden).
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Reset (delete) a team's license policy
      tags:
        - license-policies
    get:
      operationId: get_team_policy_endpoint_v1_license_policies_teams__team_id__get
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: The effective policy (team override, else org default).
        '403':
          description: Caller is not a member of this team.
        '404':
          description: >-
            No enabled policy applies (the team falls back to the static
            catalog).
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Read the effective license policy for a team
      tags:
        - license-policies
    put:
      operationId: upsert_team_policy_endpoint_v1_license_policies_teams__team_id__put
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LicensePolicyUpsertIn'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: Policy created or updated (idempotent upsert).
        '403':
          description: Caller is not a team_admin of this team.
        '404':
          description: Team not found (existence-hidden).
        '409':
          description: Uniqueness conflict on the (org, team) scope.
        '422':
          description: Malformed / oversized policy payload.
      summary: Create or update a team's license policy
      tags:
        - license-policies
  /v1/license-policies/teams/{team_id}/exceptions:
    delete:
      operationId: >-
        remove_team_exception_endpoint_v1_license_policies_teams__team_id__exceptions_delete
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
        - in: query
          name: spdx_id
          required: true
          schema:
            maxLength: 128
            minLength: 1
            title: Spdx Id
            type: string
        - in: query
          name: component_purl
          required: false
          schema:
            anyOf:
              - maxLength: 512
                type: string
              - type: 'null'
            title: Component Purl
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: Exception removed (idempotent — no-op if absent).
        '403':
          description: Caller is not a team_admin of this team.
        '404':
          description: Team or policy not found.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Un-waive (remove a license_exceptions entry from the team policy)
      tags:
        - license-policies
    post:
      operationId: >-
        add_team_exception_endpoint_v1_license_policies_teams__team_id__exceptions_post
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            format: uuid
            title: Team Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LicenseException'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePolicyOut'
          description: Exception added/updated (idempotent on spdx_id+purl).
        '403':
          description: Caller is not a team_admin of this team.
        '404':
          description: Team not found (existence-hidden).
        '422':
          description: Malformed exception, or too many exceptions.
      summary: Waive a license (add a license_exceptions entry to the team policy)
      tags:
        - license-policies
  /v1/license_findings/{finding_id}:
    get:
      operationId: get_license_finding_endpoint_v1_license_findings__finding_id__get
      parameters:
        - in: path
          name: finding_id
          required: true
          schema:
            format: uuid
            title: Finding Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicenseDetailResponse'
          description: Successful Response
        '404':
          description: >-
            Finding does not exist, or exists in a team the caller cannot
            access. Returned in lieu of 403 to avoid leaking existence.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: License finding drawer payload (404 if invisible to caller)
      tags:
        - licenses
  /v1/notifications:
    get:
      operationId: list_notifications_endpoint_v1_notifications_get
      parameters:
        - in: query
          name: unread_only
          required: false
          schema:
            default: false
            title: Unread Only
            type: boolean
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 20
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated list of notifications for the authenticated user
      tags:
        - notifications
  /v1/notifications/read-all:
    patch:
      operationId: mark_all_read_endpoint_v1_notifications_read_all_patch
      responses:
        '204':
          description: Successful Response
      summary: Mark all of the caller's unread notifications as read
      tags:
        - notifications
  /v1/notifications/unread-count:
    get:
      operationId: unread_count_endpoint_v1_notifications_unread_count_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnreadCountOut'
          description: Successful Response
      summary: Unread notification count for the authenticated user (bell badge)
      tags:
        - notifications
  /v1/notifications/{notification_id}/read:
    patch:
      operationId: mark_read_endpoint_v1_notifications__notification_id__read_patch
      parameters:
        - in: path
          name: notification_id
          required: true
          schema:
            format: uuid
            title: Notification Id
            type: string
      responses:
        '204':
          description: Marked as read (or was already read — idempotent).
        '404':
          description: >-
            Notification does not exist OR belongs to a different user
            (existence-hide).
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Mark a single notification as read (idempotent)
      tags:
        - notifications
  /v1/projects:
    get:
      operationId: list_projects_endpoint_v1_projects_get
      parameters:
        - in: query
          name: team_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            title: Team Id
        - in: query
          name: include_archived
          required: false
          schema:
            default: false
            title: Include Archived
            type: boolean
        - in: query
          name: q
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Q
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: size
          required: false
          schema:
            default: 20
            maximum: 100
            minimum: 1
            title: Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List projects visible to the caller
      tags:
        - projects
    post:
      operationId: create_project_endpoint_v1_projects_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create a project (auth required, role >= developer)
      tags:
        - projects
  /v1/projects/{project_id}:
    delete:
      operationId: delete_project_endpoint_v1_projects__project_id__delete
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Archive (soft-delete) the project (developer and above)
      tags:
        - projects
    get:
      operationId: get_project_endpoint_v1_projects__project_id__get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Read one project (IDOR-safe; 403 if not a team member)
      tags:
        - projects
    patch:
      operationId: update_project_endpoint_v1_projects__project_id__patch
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update mutable project fields (role >= team_admin)
      tags:
        - projects
  /v1/projects/{project_id}/compliance:
    get:
      operationId: list_project_compliance_endpoint_v1_projects__project_id__compliance_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 50
            maximum: 500
            minimum: 1
            title: Limit
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            title: Offset
            type: integer
        - description: >-
            Filter rows by license category. Repeat the parameter to OR-join
            multiple values (e.g. ?category=forbidden&category=conditional).
          in: query
          name: category
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            description: >-
              Filter rows by license category. Repeat the parameter to OR-join
              multiple values (e.g. ?category=forbidden&category=conditional).
            title: Category
        - description: >-
            Filter rows to licenses that carry at least one obligation of the
            given kind. Repeat to OR-join.
          in: query
          name: kind
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            description: >-
              Filter rows to licenses that carry at least one obligation of the
              given kind. Repeat to OR-join.
            title: Kind
        - description: >-
            Substring match against SPDX id and license name. LIKE
            metacharacters are escaped server-side.
          in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            description: >-
              Substring match against SPDX id and license name. LIKE
              metacharacters are escaped server-side.
            title: Search
        - description: >-
            When true, return only licenses that carry at least one obligation
            row. When false, return only licenses with NONE. Ignored when
            ``kind`` is also given.
          in: query
          name: has_obligations
          required: false
          schema:
            anyOf:
              - type: boolean
              - type: 'null'
            description: >-
              When true, return only licenses that carry at least one obligation
              row. When false, return only licenses with NONE. Ignored when
              ``kind`` is also given.
            title: Has Obligations
        - in: query
          name: sort
          required: false
          schema:
            default: category
            pattern: ^(category|license_name|spdx_id|affected_count)$
            title: Sort
            type: string
        - in: query
          name: order
          required: false
          schema:
            default: desc
            pattern: ^(asc|desc)$
            title: Order
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, the grid
            reflects this specific succeeded scan instead of the project's
            latest succeeded scan. Must belong to this project and be succeeded,
            else 404.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, the
              grid reflects this specific succeeded scan instead of the
              project's latest succeeded scan. Must belong to this project and
              be succeeded, else 404.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Unified Compliance grid (licenses × obligations) for the project's
        latest succeeded scan
      tags:
        - compliance
  /v1/projects/{project_id}/components:
    get:
      operationId: list_project_components_endpoint_v1_projects__project_id__components_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 50
            maximum: 500
            minimum: 1
            title: Limit
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            title: Offset
            type: integer
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
        - in: query
          name: severity
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Severity
        - in: query
          name: license_category
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: License Category
        - description: >-
            W2 #31 — Direct/Transitive toggle. ``true`` keeps only direct deps
            (graph depth 1), ``false`` only transitive (or graph-less) deps.
            Omit to include both. BD-equivalent of the 'Dependency type' facet.
          in: query
          name: direct
          required: false
          schema:
            anyOf:
              - type: boolean
              - type: 'null'
            description: >-
              W2 #31 — Direct/Transitive toggle. ``true`` keeps only direct deps
              (graph depth 1), ``false`` only transitive (or graph-less) deps.
              Omit to include both. BD-equivalent of the 'Dependency type'
              facet.
            title: Direct
        - description: >-
            W2 #31 — BD-style 'Usage' facet. Repeatable; accepted values:
            ``required``, ``optional``, ``unspecified`` (the NULL-scope bucket —
            common for SBOMs that don't encode scope). Unknown values are
            dropped, so a query that filters only by unknown values returns an
            empty page (not a 422). Omit to include all.
          in: query
          name: dependency_scope
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            description: >-
              W2 #31 — BD-style 'Usage' facet. Repeatable; accepted values:
              ``required``, ``optional``, ``unspecified`` (the NULL-scope bucket
              — common for SBOMs that don't encode scope). Unknown values are
              dropped, so a query that filters only by unknown values returns an
              empty page (not a 422). Omit to include all.
            title: Dependency Scope
        - in: query
          name: sort
          required: false
          schema:
            default: name
            pattern: ^(name|severity|license)$
            title: Sort
            type: string
        - in: query
          name: order
          required: false
          schema:
            default: asc
            pattern: ^(asc|desc)$
            title: Order
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, list
            components of this SPECIFIC succeeded scan instead of the project's
            latest succeeded scan. Must belong to this project and be succeeded,
            else 404. Omit for the default latest-succeeded behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, list
              components of this SPECIFIC succeeded scan instead of the
              project's latest succeeded scan. Must belong to this project and
              be succeeded, else 404. Omit for the default latest-succeeded
              behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComponentListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated component list for the project's latest scan
      tags:
        - projects
  /v1/projects/{project_id}/diff:
    get:
      operationId: diff_project_releases_endpoint_v1_projects__project_id__diff_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: >-
            Base snapshot scan id (typically the OLDER release, e.g. v0.1). Must
            belong to this project and be succeeded, else 404 (existence-hide).
          in: query
          name: base
          required: true
          schema:
            description: >-
              Base snapshot scan id (typically the OLDER release, e.g. v0.1).
              Must belong to this project and be succeeded, else 404
              (existence-hide).
            format: uuid
            title: Base
            type: string
        - description: >-
            Target snapshot scan id (typically the NEWER release, e.g. v0.2).
            Must belong to this project and be succeeded, else 404. `base ==
            target` is allowed and yields an all-empty diff.
          in: query
          name: target
          required: true
          schema:
            description: >-
              Target snapshot scan id (typically the NEWER release, e.g. v0.2).
              Must belong to this project and be succeeded, else 404. `base ==
              target` is allowed and yields an all-empty diff.
            format: uuid
            title: Target
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectDiff'
          description: Successful Response
        '403':
          content:
            application/problem+json: {}
          description: >-
            Caller is not a member of the project's owning team (super_admin
            bypasses). RFC 7807 problem+json.
        '404':
          content:
            application/problem+json: {}
          description: >-
            Project does not exist, or one of `base`/`target` is not a succeeded
            scan of THIS project (existence-hidden: nonexistent / another
            project's scan / non-succeeded all collapse to the same 404). RFC
            7807 problem+json.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Diff two release snapshots (succeeded scans) of the project
      tags:
        - projects
  /v1/projects/{project_id}/gate-result:
    get:
      operationId: get_gate_result_endpoint_v1_projects__project_id__gate_result_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, evaluate
            the build gate against this SPECIFIC succeeded scan instead of the
            project's latest succeeded scan (so the Overview gate card can
            reflect a pinned release). Must belong to this project and be
            succeeded, else 404. Omit for the default latest-succeeded behaviour
            (the CI contract).
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given,
              evaluate the build gate against this SPECIFIC succeeded scan
              instead of the project's latest succeeded scan (so the Overview
              gate card can reflect a pinned release). Must belong to this
              project and be succeeded, else 404. Omit for the default
              latest-succeeded behaviour (the CI contract).
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GateResultResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Evaluate the build-gate verdict for the project's latest succeeded scan
      tags:
        - policy-gate
  /v1/projects/{project_id}/licenses:
    get:
      operationId: list_project_licenses_endpoint_v1_projects__project_id__licenses_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 50
            maximum: 500
            minimum: 1
            title: Limit
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            title: Offset
            type: integer
        - in: query
          name: category
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Category
        - in: query
          name: kind
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Kind
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
        - in: query
          name: sort
          required: false
          schema:
            default: category
            pattern: ^(category|name|spdx_id|affected_count)$
            title: Sort
            type: string
        - in: query
          name: order
          required: false
          schema:
            default: desc
            pattern: ^(asc|desc)$
            title: Order
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, list
            license rows of this SPECIFIC succeeded scan instead of the
            project's latest succeeded scan. Must belong to this project and be
            succeeded, else 404. Omit for the default latest-succeeded
            behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, list
              license rows of this SPECIFIC succeeded scan instead of the
              project's latest succeeded scan. Must belong to this project and
              be succeeded, else 404. Omit for the default latest-succeeded
              behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicenseListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Per-license rows + category distribution for the project's latest scan
      tags:
        - licenses
  /v1/projects/{project_id}/notice:
    get:
      operationId: get_project_notice_endpoint_v1_projects__project_id__notice_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: >-
            Output format. ``text`` returns text/plain, ``markdown`` returns
            text/markdown, ``html`` returns a self-contained text/html document.
          in: query
          name: format
          required: false
          schema:
            default: text
            description: >-
              Output format. ``text`` returns text/plain, ``markdown`` returns
              text/markdown, ``html`` returns a self-contained text/html
              document.
            pattern: ^(text|markdown|html)$
            title: Format
            type: string
        - description: >-
            When true, set ``Content-Disposition: attachment`` so browsers save
            the body as a file. Default is inline.
          in: query
          name: download
          required: false
          schema:
            default: false
            description: >-
              When true, set ``Content-Disposition: attachment`` so browsers
              save the body as a file. Default is inline.
            title: Download
            type: boolean
      responses:
        '200':
          content:
            application/json:
              schema: {}
            text/html: {}
            text/markdown: {}
            text/plain: {}
          description: >-
            NOTICE body in the requested format (plain text, markdown, or a
            self-contained HTML document). ``Content-Disposition`` is set to
            ``attachment`` when ``download=true``; otherwise the body streams
            inline.
        '404':
          description: >-
            Project does not exist, or the caller is not a member of the
            project's team (existence-hidden).
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
        '429':
          content:
            application/problem+json: {}
          description: Rate limit exceeded for this client IP.
      summary: Compose a NOTICE attribution body for the project's latest scan
      tags:
        - obligations
  /v1/projects/{project_id}/obligations:
    get:
      operationId: >-
        list_project_obligations_endpoint_v1_projects__project_id__obligations_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 50
            maximum: 500
            minimum: 1
            title: Limit
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            title: Offset
            type: integer
        - in: query
          name: kind
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Kind
        - in: query
          name: category
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Category
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
        - in: query
          name: sort
          required: false
          schema:
            default: category
            pattern: ^(category|license_name|kind|affected_count)$
            title: Sort
            type: string
        - in: query
          name: order
          required: false
          schema:
            default: desc
            pattern: ^(asc|desc)$
            title: Order
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, list
            obligation rows of this SPECIFIC succeeded scan instead of the
            project's latest succeeded scan. Must belong to this project and be
            succeeded, else 404. Omit for the default latest-succeeded
            behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, list
              obligation rows of this SPECIFIC succeeded scan instead of the
              project's latest succeeded scan. Must belong to this project and
              be succeeded, else 404. Omit for the default latest-succeeded
              behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObligationListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Per-(license, kind) obligation rows + distribution for the project's
        latest scan
      tags:
        - obligations
  /v1/projects/{project_id}/obligations/{obligation_id}:
    get:
      operationId: >-
        get_obligation_endpoint_v1_projects__project_id__obligations__obligation_id__get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: path
          name: obligation_id
          required: true
          schema:
            format: uuid
            title: Obligation Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObligationDetailResponse'
          description: Successful Response
        '404':
          description: >-
            Obligation does not exist, exists in a team the caller cannot
            access, or is not surfaced by the project's latest scan. Returned in
            lieu of 403 to avoid leaking existence.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Obligation drawer payload (404 if invisible to caller within this
        project)
      tags:
        - obligations
  /v1/projects/{project_id}/overview:
    get:
      operationId: get_project_overview_endpoint_v1_projects__project_id__overview_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given,
            aggregate this SPECIFIC succeeded scan instead of the project's
            latest succeeded scan. Must belong to this project and be succeeded,
            else 404. Omit for the default latest-succeeded behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given,
              aggregate this SPECIFIC succeeded scan instead of the project's
              latest succeeded scan. Must belong to this project and be
              succeeded, else 404. Omit for the default latest-succeeded
              behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectOverviewResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Aggregated risk / scan picture for the project (Phase 3 Overview tab)
      tags:
        - projects
  /v1/projects/{project_id}/releases:
    get:
      operationId: list_project_releases_endpoint_v1_projects__project_id__releases_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: size
          required: false
          schema:
            default: 20
            maximum: 100
            minimum: 1
            title: Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReleaseListResponse'
          description: Successful Response
        '403':
          content:
            application/problem+json: {}
          description: >-
            Caller is not a member of the project's owning team (super_admin
            bypasses). RFC 7807 problem+json.
        '404':
          content:
            application/problem+json: {}
          description: Project does not exist. RFC 7807 problem+json.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List the project's release snapshots (succeeded scans, newest-first)
      tags:
        - projects
  /v1/projects/{project_id}/remediation/npm/dry-run:
    post:
      operationId: post_npm_dry_run_v1_projects__project_id__remediation_npm_dry_run_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              anyOf:
                - $ref: '#/components/schemas/NpmDryRunRequest'
                - type: 'null'
              title: Body
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NpmDryRunResponse'
          description: Computed dry-run (may be a no-op / no-manifest).
        '401':
          description: Authentication required
        '404':
          description: Project not found / not accessible
        '422':
          description: Supplied/fetched package.json could not be edited
      summary: Preview the npm dependency-bump edit for a project (dry-run, no PR)
      tags:
        - remediation
  /v1/projects/{project_id}/remediation/npm/pull-request:
    post:
      description: >-
        Open (or return the existing) automated npm remediation PR.


        team_admin RBAC + opt-in enforcement live in the service. Returns 201
        for a

        freshly opened PR, 200 for an idempotent hit on an existing open PR, and
        204

        when there is nothing to remediate.
      operationId: >-
        post_npm_pull_request_v1_projects__project_id__remediation_npm_pull_request_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              anyOf:
                - $ref: '#/components/schemas/NpmPullRequestCreate'
                - type: 'null'
              title: Body
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemediationPullRequestOut'
          description: An existing open PR was returned (idempotent hit).
        '201':
          description: A new remediation PR was opened.
        '204':
          description: Nothing to remediate (no manifest change).
        '401':
          description: Authentication required
        '403':
          description: Caller is not a team_admin of the project's team
        '404':
          description: Project not found / not accessible
        '409':
          description: Project is not opted in to automated remediation PRs
        '422':
          description: Manifest / stored config unusable
        '502':
          description: A GitHub write failed
      summary: Open an automated npm remediation PR on the project's opted-in repo
      tags:
        - remediation
  /v1/projects/{project_id}/remediation/pull-requests:
    get:
      operationId: >-
        get_remediation_pull_requests_v1_projects__project_id__remediation_pull_requests_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: page_size
          required: false
          schema:
            default: 50
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemediationPullRequestList'
          description: The project's remediation-PR records (newest first).
        '401':
          description: Authentication required
        '404':
          description: Project not found / not accessible
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List the project's automated remediation PR records
      tags:
        - remediation
  /v1/projects/{project_id}/reports/history:
    get:
      operationId: >-
        list_project_report_history_endpoint_v1_projects__project_id__reports_history_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: >-
            Optional filter — one or more report_type values to include. Repeat
            the parameter (``?type=notice&type=sbom``) for multi-select. Omit
            for all four types.
          in: query
          name: type
          required: false
          schema:
            anyOf:
              - items:
                  enum:
                    - notice
                    - sbom
                    - vuln_pdf
                    - vex_export
                  type: string
                type: array
              - type: 'null'
            description: >-
              Optional filter — one or more report_type values to include.
              Repeat the parameter (``?type=notice&type=sbom``) for
              multi-select. Omit for all four types.
            title: Type
        - description: >-
            Optional filter — return only rows where ``scan_id`` matches. Pair
            with ``type=sbom`` etc. to find all artefacts produced for one scan.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional filter — return only rows where ``scan_id`` matches. Pair
              with ``type=sbom`` etc. to find all artefacts produced for one
              scan.
            title: Scan Id
        - description: 1-based page number.
          in: query
          name: page
          required: false
          schema:
            default: 1
            description: 1-based page number.
            minimum: 1
            title: Page
            type: integer
        - description: Rows per page (1..200, default 50).
          in: query
          name: page_size
          required: false
          schema:
            default: 50
            description: Rows per page (1..200, default 50).
            maximum: 200
            minimum: 1
            title: Page Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportHistoryResponse'
          description: Paginated report-download history (newest first).
        '401':
          description: Authentication required
        '404':
          content:
            application/problem+json: {}
          description: >-
            Project does not exist, or the caller is not a member of its team
            (existence-hidden — same envelope either way).
        '422':
          content:
            application/problem+json: {}
          description: >-
            Invalid query parameter (unknown report_type, malformed scan_id,
            page/page_size out of range).
        '429':
          content:
            application/problem+json: {}
          description: Rate limit exceeded for this client.
      summary: List download / export history for the project's Reports center
      tags:
        - reports
  /v1/projects/{project_id}/sbom:
    get:
      operationId: export_project_sbom_endpoint_v1_projects__project_id__sbom_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: SBOM output format.
          in: query
          name: format
          required: false
          schema:
            default: cyclonedx-json
            description: SBOM output format.
            enum:
              - cyclonedx-json
              - cyclonedx-xml
              - spdx-json
              - spdx-tv
            title: Format
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, export
            this SPECIFIC succeeded scan instead of the project's latest
            succeeded scan. Must belong to this project and be succeeded, else
            404. Omit for the default latest-succeeded behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, export
              this SPECIFIC succeeded scan instead of the project's latest
              succeeded scan. Must belong to this project and be succeeded, else
              404. Omit for the default latest-succeeded behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/spdx+json: {}
            application/vnd.cyclonedx+json: {}
            application/vnd.cyclonedx+xml: {}
            text/spdx: {}
          description: SBOM document download
        '401':
          description: Authentication required
        '404':
          description: Project not found or not accessible
        '422':
          description: Unknown SBOM format
      summary: Export SBOM for the project's latest succeeded scan
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/attestation:
    get:
      operationId: >-
        download_sbom_attestation_endpoint_v1_projects__project_id__sbom_attestation_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/octet-stream: {}
          description: in-toto / DSSE SLSA provenance attestation
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or no attestation exists
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download the in-toto / SLSA provenance attestation for the latest SBOM
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/attestation-certificate:
    get:
      operationId: >-
        download_sbom_attestation_certificate_endpoint_v1_projects__project_id__sbom_attestation_certificate_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/x-pem-file: {}
          description: >-
            Fulcio certificate for the in-toto attestation (keyless
            verification)
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or no attestation certificate exists
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Download the Fulcio certificate for the attestation (keyless signing
        only)
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/certificate:
    get:
      operationId: >-
        download_sbom_certificate_endpoint_v1_projects__project_id__sbom_certificate_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/x-pem-file: {}
          description: Fulcio signing certificate (keyless verification)
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or no keyless certificate exists
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download the Fulcio signing certificate (keyless signing only)
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/public-key:
    get:
      operationId: >-
        download_sbom_public_key_endpoint_v1_projects__project_id__sbom_public_key_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/x-pem-file: {}
          description: cosign PUBLIC key (verify with cosign verify-blob --key)
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or no public key is configured
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download the cosign public key for verifying SBOM signatures
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/signature:
    get:
      operationId: >-
        download_sbom_signature_endpoint_v1_projects__project_id__sbom_signature_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/octet-stream: {}
          description: Detached cosign signature (verify with cosign verify-blob)
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or the SBOM was not signed
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download the detached cosign signature for the latest SBOM
      tags:
        - sbom
  /v1/projects/{project_id}/sbom/signature-bundle:
    get:
      operationId: >-
        download_sbom_signature_bundle_endpoint_v1_projects__project_id__sbom_signature_bundle_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/zip: {}
          description: Zip bundle for external cosign verification
        '401':
          description: Authentication required
        '404':
          description: Project not accessible, or the SBOM was not signed
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Download a zip bundle (SBOM + signature + cert/public-key + attestation
        + README)
      tags:
        - sbom
  /v1/projects/{project_id}/scans:
    get:
      operationId: list_scans_endpoint_v1_projects__project_id__scans_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: size
          required: false
          schema:
            default: 20
            maximum: 100
            minimum: 1
            title: Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List scans for a project (most recent first)
      tags:
        - scans
    post:
      operationId: trigger_scan_endpoint_v1_projects__project_id__scans_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanCreate'
        required: true
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
        '429':
          content:
            application/problem+json: {}
          description: >-
            Rate limited (too many triggers from this user) or the team's
            concurrent-scan cap is reached. RFC 7807 problem+json with a
            Retry-After header; the concurrency-cap variant adds a `limit`
            extension field. (The live per-team active-scan count is
            intentionally not exposed — see M1.)
      summary: >-
        Trigger a scan for the project (queues a Celery task; returns 202
        Accepted)
      tags:
        - projects
  /v1/projects/{project_id}/source-archive:
    post:
      operationId: >-
        upload_source_archive_endpoint_v1_projects__project_id__source_archive_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: >-
                #/components/schemas/Body_upload_source_archive_endpoint_v1_projects__project_id__source_archive_post
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceArchiveUploadResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: >-
        Upload a .zip of local source for scanning (auth required, role >=
        developer)
      tags:
        - projects
  /v1/projects/{project_id}/source-file:
    get:
      operationId: get_source_file_v1_projects__project_id__source_file_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: File to read, relative to the source root.
          in: query
          name: path
          required: true
          schema:
            description: File to read, relative to the source root.
            title: Path
            type: string
        - description: Scan to read; defaults to the project's latest scan.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: Scan to read; defaults to the project's latest scan.
            title: Scan Id
        - description: >-
            When true, stream the FULL member as application/octet-stream (no
            per-file viewer cap) for download instead of the capped JSON
            preview. Same path-traversal / symlink defences apply.
          in: query
          name: raw
          required: false
          schema:
            default: false
            description: >-
              When true, stream the FULL member as application/octet-stream (no
              per-file viewer cap) for download instead of the capped JSON
              preview. Same path-traversal / symlink defences apply.
            title: Raw
            type: boolean
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceFileResponse'
            application/octet-stream: {}
          description: >-
            Capped file JSON (default), or — with ``raw=true`` — the FULL member
            streamed as application/octet-stream.
        '400':
          description: Malformed path selector
        '401':
          description: Authentication required
        '404':
          description: Project / scan / file not available
        '413':
          description: Requested member is a directory / non-regular file
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Read one file from a scan's preserved source + per-line license matches
      tags:
        - source-tree
  /v1/projects/{project_id}/source-tree:
    get:
      operationId: get_source_tree_v1_projects__project_id__source_tree_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: Directory whose immediate children to list. Empty = root.
          in: query
          name: path
          required: false
          schema:
            default: ''
            description: Directory whose immediate children to list. Empty = root.
            title: Path
            type: string
        - description: 1-based page index.
          in: query
          name: page
          required: false
          schema:
            default: 1
            description: 1-based page index.
            minimum: 1
            title: Page
            type: integer
        - description: Page size (max 500).
          in: query
          name: size
          required: false
          schema:
            default: 100
            description: Page size (max 500).
            maximum: 500
            minimum: 1
            title: Size
            type: integer
        - description: Scan to read; defaults to the project's latest scan.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: Scan to read; defaults to the project's latest scan.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceTreePage'
          description: Successful Response
        '400':
          description: Malformed path selector
        '401':
          description: Authentication required
        '404':
          description: Project / scan / preserved source not available
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List immediate children of a directory in a scan's preserved source
      tags:
        - source-tree
  /v1/projects/{project_id}/vex:
    get:
      operationId: export_project_vex_endpoint_v1_projects__project_id__vex_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - description: VEX output format.
          in: query
          name: format
          required: false
          schema:
            default: openvex
            description: VEX output format.
            enum:
              - openvex
              - cyclonedx
            title: Format
            type: string
      responses:
        '200':
          content:
            application/json: {}
          description: VEX document download
        '401':
          description: Authentication required
        '404':
          description: Project not found or not accessible
        '422':
          description: Unknown VEX format
      summary: Export a VEX document from the project's current finding triage
      tags:
        - vex
  /v1/projects/{project_id}/vex/import:
    post:
      operationId: import_project_vex_endpoint_v1_projects__project_id__vex_import_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: >-
                #/components/schemas/Body_import_project_vex_endpoint_v1_projects__project_id__vex_import_post
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VEXImportSummary'
          description: Import summary (matched/applied/skipped/errors)
        '401':
          description: Authentication required
        '403':
          description: Requires team_admin within the project's team
        '404':
          description: Project not found or not accessible
        '413':
          description: VEX document too large
        '422':
          description: Malformed or unsupported VEX document
      summary: Import a VEX document, auto-transitioning matching findings (team_admin)
      tags:
        - vex
  /v1/projects/{project_id}/vulnerabilities:
    get:
      operationId: >-
        list_project_vulnerabilities_endpoint_v1_projects__project_id__vulnerabilities_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 50
            maximum: 500
            minimum: 1
            title: Limit
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            title: Offset
            type: integer
        - in: query
          name: search
          required: false
          schema:
            anyOf:
              - maxLength: 255
                type: string
              - type: 'null'
            title: Search
        - in: query
          name: severity
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Severity
        - in: query
          name: status
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Status
        - description: >-
            W2 #33 — License risk-axis filter. Repeatable; accepted values:
            ``forbidden``, ``conditional``, ``allowed``, ``unknown`` (the cv had
            no license finding in this scan). Unknown values are dropped, so a
            query that filters ONLY by unknown values returns an empty page (not
            a 422). Omit to include all categories.
          in: query
          name: license_category
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            description: >-
              W2 #33 — License risk-axis filter. Repeatable; accepted values:
              ``forbidden``, ``conditional``, ``allowed``, ``unknown`` (the cv
              had no license finding in this scan). Unknown values are dropped,
              so a query that filters ONLY by unknown values returns an empty
              page (not a 422). Omit to include all categories.
            title: License Category
        - description: >-
            Keep only findings whose CVE has an EPSS exploit-probability >= this
            threshold, in [0, 1]. CVEs with no published EPSS score are
            excluded. Omit to disable EPSS filtering.
          in: query
          name: min_epss
          required: false
          schema:
            anyOf:
              - maximum: 1
                minimum: 0
                type: number
              - type: 'null'
            description: >-
              Keep only findings whose CVE has an EPSS exploit-probability >=
              this threshold, in [0, 1]. CVEs with no published EPSS score are
              excluded. Omit to disable EPSS filtering.
            title: Min Epss
        - description: >-
            Tri-state reachability filter (v2.3). ``true`` → only findings whose
            vulnerable symbol is reachable on the call graph; ``false`` → only
            findings an analyser proved NOT reachable; ``unknown`` → only
            not-analysed findings (reachable IS NULL). Omit to disable the
            reachability filter.
          in: query
          name: reachable
          required: false
          schema:
            anyOf:
              - pattern: ^(true|false|unknown)$
                type: string
              - type: 'null'
            description: >-
              Tri-state reachability filter (v2.3). ``true`` → only findings
              whose vulnerable symbol is reachable on the call graph; ``false``
              → only findings an analyser proved NOT reachable; ``unknown`` →
              only not-analysed findings (reachable IS NULL). Omit to disable
              the reachability filter.
            title: Reachable
        - description: >-
            Sort key. ``reachable`` ranks reachable findings first (then
            not-analysed, then proven-unreachable), tie-broken by severity desc.
            ``component`` sorts by affected package name.
          in: query
          name: sort
          required: false
          schema:
            default: severity
            description: >-
              Sort key. ``reachable`` ranks reachable findings first (then
              not-analysed, then proven-unreachable), tie-broken by severity
              desc. ``component`` sorts by affected package name.
            pattern: ^(severity|cvss|status|discovered_at|epss|reachable|component)$
            title: Sort
            type: string
        - in: query
          name: order
          required: false
          schema:
            default: desc
            pattern: ^(asc|desc)$
            title: Order
            type: string
        - description: >-
            Optional release-snapshot anchor (feature #28). When given, list CVE
            findings of this SPECIFIC succeeded scan instead of the project's
            latest succeeded scan. Must belong to this project and be succeeded,
            else 404. Omit for the default latest-succeeded behaviour.
          in: query
          name: scan_id
          required: false
          schema:
            anyOf:
              - format: uuid
                type: string
              - type: 'null'
            description: >-
              Optional release-snapshot anchor (feature #28). When given, list
              CVE findings of this SPECIFIC succeeded scan instead of the
              project's latest succeeded scan. Must belong to this project and
              be succeeded, else 404. Omit for the default latest-succeeded
              behaviour.
            title: Scan Id
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VulnerabilityListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Paginated CVE findings for the project's latest scan
      tags:
        - vulnerabilities
  /v1/projects/{project_id}/vulnerabilities:bulk-transition:
    post:
      description: >-
        W2 #33b — apply one VEX transition across many findings in one
        round-trip.


        Per-row failures (404 / 403 / 422) are surfaced in the response envelope

        so the UI can render "succeeded N · failed M" with per-row details.

        Only envelope-level shape violations (empty list, > cap, unknown enum)

        return RFC 7807 — those would still abort a per-row partial commit, so

        they belong on the envelope rather than masquerading as per-row
        outcomes.
      operationId: >-
        bulk_transition_vulnerabilities_endpoint_v1_projects__project_id__vulnerabilities_bulk_transition_post
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VulnerabilityBulkStatusUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VulnerabilityBulkStatusResponse'
          description: >-
            Bulk envelope completed. ``results[*].status_code`` reports per-row
            outcomes (200/403/404/422). ``succeeded`` + ``failed`` == ``total``.
        '404':
          description: >-
            Project does not exist, OR the caller is not a member of the
            project's team. Returned in lieu of 403 to avoid leaking team
            membership (mirrors the single-row PATCH existence-hide policy).
        '422':
          description: >-
            Envelope-level shape violation: empty ``finding_ids``, more than
            ``BULK_TRANSITION_MAX`` entries, unknown ``target_status``. Per-row
            matrix violations are NOT envelope 422 — they are reported as
            ``results[*].status_code == 422``.
      summary: Transition many findings in one project to the same VEX status
      tags:
        - vulnerabilities
  /v1/projects/{project_id}/vulnerability-report.pdf:
    get:
      operationId: >-
        get_vulnerability_report_pdf_endpoint_v1_projects__project_id__vulnerability_report_pdf_get
      parameters:
        - in: path
          name: project_id
          required: true
          schema:
            format: uuid
            title: Project Id
            type: string
      responses:
        '200':
          content:
            application/pdf: {}
          description: PDF report download
        '401':
          description: Authentication required
        '404':
          description: Project not found or not accessible
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
        '500':
          content:
            application/problem+json: {}
          description: PDF rendering failed (e.g. weasyprint unavailable)
      summary: Download a vulnerability PDF report for the project's latest scan
      tags:
        - reports
  /v1/scans:
    get:
      operationId: list_my_scans_endpoint_v1_scans_get
      parameters:
        - description: Filter by scan status.
          in: query
          name: status
          required: false
          schema:
            anyOf:
              - pattern: ^(queued|running|succeeded|failed|cancelled)$
                type: string
              - type: 'null'
            description: Filter by scan status.
            title: Status
        - in: query
          name: page
          required: false
          schema:
            default: 1
            minimum: 1
            title: Page
            type: integer
        - in: query
          name: size
          required: false
          schema:
            default: 20
            maximum: 100
            minimum: 1
            title: Size
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanListResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List scans across every project accessible to the caller
      tags:
        - scans
  /v1/scans/{scan_id}:
    delete:
      description: >-
        Hard-delete a terminal scan and (via cascade) its findings / components.


        DT-style retention reclaims most stale scans automatically; this is the

        manual escape hatch. Auth: any team member (``developer``+). The
        owning-team

        check lives in the service (``delete_scan``), which existence-hides
        other

        teams' scans as 404. Active scans (queued/running) return 409 — cancel

        first. A release-labelled scan returns 409 unless ``force=true``.
      operationId: delete_scan_endpoint_v1_scans__scan_id__delete
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
        - description: >-
            Delete even when the scan carries an explicit metadata.release
            label. Release-labelled snapshots are immutable by default.
          in: query
          name: force
          required: false
          schema:
            default: false
            description: >-
              Delete even when the scan carries an explicit metadata.release
              label. Release-labelled snapshots are immutable by default.
            title: Force
            type: boolean
      responses:
        '204':
          description: Scan deleted (cascade removed its findings).
        '404':
          description: Scan not found, or not visible to the caller.
        '409':
          description: >-
            Scan is active (cancel it first) or carries a release label (pass
            ``force=true`` to delete).
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete a terminal scan and its findings (own-team scans only)
      tags:
        - scans
    get:
      operationId: get_scan_endpoint_v1_scans__scan_id__get
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanPublic'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Read one scan (IDOR-safe via project team membership)
      tags:
        - scans
  /v1/scans/{scan_id}/cancel:
    post:
      description: >-
        Cancel one of the caller's own team's scans.


        PR-A1 (scan stability). Auth: any authenticated team member

        (``developer`` or higher). The owning-team check lives in the service

        (``cancel_scan_for_actor``) which existence-hides other teams' scans as

        404 — so a developer cannot probe scan ids belonging to other teams.


        Admin force-cancel (``POST /v1/admin/scans/{id}/cancel``) remains
        separate

        and cross-team; the two share the same revoke + status-mutation core.
      operationId: cancel_scan_endpoint_v1_scans__scan_id__cancel_post
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Cancel a queued / running scan owned by the caller's team
      tags:
        - scans
  /v1/scans/{scan_id}/log:
    get:
      description: >-
        Stream the per-scan ``scan.log`` written by
        ``tasks._progress.publish_log``.


        Authorization: same gate as ``GET /v1/scans/{scan_id}`` — reuses

        ``services.scan_service.get_scan`` so team-membership / super-admin
        rules

        stay in lock-step with the metadata endpoint. A non-member sees the same

        404 as a non-existent scan id (existence-hide) so a developer cannot
        probe

        scan ids belonging to other teams via this endpoint.


        Lifecycle: the file is written incrementally by the worker as the scan

        runs. While the scan is still running the response returns whatever has

        been flushed so far (the publisher uses a line-buffered handle, so each

        completed line is on disk by the time it is on the WebSocket). After the

        scan terminates the file stays on disk until ``workspace_cleaner`` reaps

        the parent workspace directory (current default: per

        ``WORKSPACE_ORPHAN_MAX_AGE_SECONDS``).
      operationId: download_scan_log_endpoint_v1_scans__scan_id__log_get
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema: {}
            text/plain: {}
          description: Plain-text scan log, streamed.
        '404':
          content:
            application/problem+json: {}
          description: >-
            No scan log is available for this scan id. The body is deliberately
            the same for every miss path (scan not found, caller has no access,
            log file not yet on disk, or path traversal defense triggered) so
            the response envelope cannot be used to enumerate scan ids across
            teams. Operators can distinguish branches via the
            ``scan_log_not_found`` structlog event's ``reason`` field.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Download the persisted tool log for one scan
      tags:
        - scans
  /v1/scans/{scan_id}/post-pr-comment:
    post:
      operationId: post_pr_comment_endpoint_v1_scans__scan_id__post_pr_comment_post
      parameters:
        - in: path
          name: scan_id
          required: true
          schema:
            format: uuid
            title: Scan Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostPRCommentRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostPRCommentResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Render an SCA Markdown report and (optionally) post it to a GitHub PR
      tags:
        - policy-gate
  /v1/users/me/notification-prefs:
    get:
      operationId: get_notification_prefs_v1_users_me_notification_prefs_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPrefsOut'
          description: Successful Response
      summary: Return the caller's notification preferences (creates defaults)
      tags:
        - users-me
    put:
      description: |-
        Full-row update — every channel field must be supplied.

        The body's only meaningful inputs are the four channel toggles. Any
        additional fields a caller may send (``user_id``, ``id``, ...) are
        ignored: Pydantic strips unknown fields by default and the service is
        keyed off ``actor.id``, never the body.

        Chore O / M3 — In-app notifications cannot be disabled. The frontend
        documents the in-app switch as "rendered but disabled"; this server-
        side guard closes the API drift where a direct PUT could opt out.
      operationId: put_notification_prefs_v1_users_me_notification_prefs_put
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationPrefsIn'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPrefsOut'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Replace the caller's notification preferences (full-row PUT)
      tags:
        - users-me
  /v1/users/me/oauth-identities:
    get:
      operationId: list_oauth_identities_v1_users_me_oauth_identities_get
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthIdentityListResponse'
          description: Successful Response
      summary: List the caller's connected OAuth identities (sorted oldest-first)
      tags:
        - users-me
  /v1/users/me/oauth-identities/{identity_id}:
    delete:
      description: |-
        Remove an OAuth identity link from the authenticated user.

        Returns 204 on success. Domain failures map to RFC 7807:

          - 404 ``urn:trustedoss:problem:oauth_identity_not_found`` —
            identity does not exist OR belongs to another user
            (existence-hide; the two cases share a shape).
          - 409 ``urn:trustedoss:problem:oauth_unlink_blocks_login`` —
            unlinking would leave the user with no way to authenticate.
      operationId: delete_oauth_identity_v1_users_me_oauth_identities__identity_id__delete
      parameters:
        - in: path
          name: identity_id
          required: true
          schema:
            format: uuid
            title: Identity Id
            type: string
      responses:
        '204':
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Unlink one of the caller's OAuth identities
      tags:
        - users-me
  /v1/vulnerability_findings/{finding_id}:
    get:
      operationId: >-
        get_vulnerability_finding_endpoint_v1_vulnerability_findings__finding_id__get
      parameters:
        - in: path
          name: finding_id
          required: true
          schema:
            format: uuid
            title: Finding Id
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VulnerabilityDetailResponse'
          description: Successful Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Vulnerability finding drawer payload (404 if invisible to caller)
      tags:
        - vulnerabilities
  /v1/vulnerability_findings/{finding_id}/status:
    patch:
      operationId: >-
        update_vulnerability_status_endpoint_v1_vulnerability_findings__finding_id__status_patch
      parameters:
        - in: path
          name: finding_id
          required: true
          schema:
            format: uuid
            title: Finding Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VulnerabilityStatusUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VulnerabilityDetailResponse'
          description: >-
            Status transitioned, OR the finding was already at the requested
            status (idempotent no-op — M-26). Body is the post-commit detail
            payload.
        '403':
          description: >-
            Caller's role is insufficient (e.g. developer attempting `→
            suppressed`).
        '404':
          description: >-
            Finding does not exist, or exists in a team the caller cannot
            access. Returned in lieu of 403 to avoid leaking existence.
        '409':
          description: if_match snapshot did not match the current updated_at.
        '422':
          description: >-
            Transition is not allowed by the workflow matrix. The Problem
            Details body carries an `allowed_to` extension listing the legal
            next states from the current status.
      summary: Transition a vulnerability finding's VEX status (audit-logged)
      tags:
        - vulnerabilities
  /v1/webhooks/github:
    post:
      operationId: github_webhook_endpoint_v1_webhooks_github_post
      parameters:
        - in: header
          name: X-Hub-Signature-256
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Hub-Signature-256
        - in: header
          name: X-GitHub-Event
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Github-Event
        - in: header
          name: X-GitHub-Delivery
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Github-Delivery
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: >-
            Delivery accepted. Body shape: ``{"status":
            "enqueued"|"duplicate"|"ignored", "delivery_id": str, "scan_id":
            uuid?}``
        '400':
          description: Required webhook headers are missing or malformed JSON body.
        '401':
          description: HMAC verification failed.
        '404':
          description: No project configured for the payload's repository URL.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Receive a GitHub webhook delivery
      tags:
        - webhooks
  /v1/webhooks/gitlab:
    post:
      operationId: gitlab_webhook_endpoint_v1_webhooks_gitlab_post
      parameters:
        - in: header
          name: X-Gitlab-Token
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Gitlab-Token
        - in: header
          name: X-Gitlab-Event
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Gitlab-Event
        - in: header
          name: X-Gitlab-Webhook-UUID
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: X-Gitlab-Webhook-Uuid
      responses:
        '200':
          content:
            application/json:
              schema: {}
          description: >-
            Delivery accepted. Body shape: ``{"status":
            "enqueued"|"duplicate"|"ignored", "delivery_id": str, "scan_id":
            uuid?}``
        '400':
          description: Required webhook headers are missing or malformed JSON body.
        '401':
          description: X-Gitlab-Token mismatch.
        '404':
          description: No project configured for the payload's repository URL.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Receive a GitLab webhook delivery
      tags:
        - webhooks
