openapi: 3.1.0
info:
  title: Finding Risk Acceptance Internal Contract
  version: 0.1.0
  description: |
    Internal admin-plane contract for the Finding Risk Acceptance Lifecycle.
    These endpoints represent the server-side action contract backing Filament and Livewire surfaces.
servers:
  - url: /api/internal
paths:
  /tenants/{tenantId}/findings/{findingId}/exception-requests:
    post:
      summary: Request exception
      operationId: requestFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/FindingId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestExceptionInput'
      responses:
        '201':
          description: Exception request created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /tenants/{tenantId}/finding-exceptions:
    get:
      summary: List tenant exceptions
      operationId: listTenantFindingExceptions
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/StateFilter'
        - $ref: '#/components/parameters/DueFilter'
      responses:
        '200':
          description: Tenant exception register
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /tenants/{tenantId}/finding-exceptions/{exceptionId}:
    get:
      summary: View exception detail
      operationId: showFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/ExceptionId'
      responses:
        '200':
          description: Exception detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionDetail'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /tenants/{tenantId}/finding-exceptions/{exceptionId}/approve:
    post:
      summary: Approve exception
      operationId: approveFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/ExceptionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApproveExceptionInput'
      responses:
        '200':
          description: Exception approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /tenants/{tenantId}/finding-exceptions/{exceptionId}/reject:
    post:
      summary: Reject exception
      operationId: rejectFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/ExceptionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RejectExceptionInput'
      responses:
        '200':
          description: Exception rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /tenants/{tenantId}/finding-exceptions/{exceptionId}/renew:
    post:
      summary: Renew exception
      operationId: renewFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/ExceptionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RenewExceptionInput'
      responses:
        '200':
          description: Renewal request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /tenants/{tenantId}/finding-exceptions/{exceptionId}/revoke:
    post:
      summary: Revoke exception
      operationId: revokeFindingException
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/TenantId'
        - $ref: '#/components/parameters/ExceptionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RevokeExceptionInput'
      responses:
        '200':
          description: Exception revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /workspaces/{workspaceId}/finding-exceptions/queue:
    get:
      summary: Canonical exception approval queue
      operationId: listCanonicalFindingExceptionQueue
      tags: [Finding Exceptions]
      parameters:
        - $ref: '#/components/parameters/WorkspaceId'
        - $ref: '#/components/parameters/StateFilter'
        - $ref: '#/components/parameters/DueFilter'
        - name: tenantId
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Canonical queue filtered to entitled tenants
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FindingExceptionResource'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    TenantId:
      name: tenantId
      in: path
      required: true
      schema:
        type: integer
    FindingId:
      name: findingId
      in: path
      required: true
      schema:
        type: integer
    ExceptionId:
      name: exceptionId
      in: path
      required: true
      schema:
        type: integer
    WorkspaceId:
      name: workspaceId
      in: path
      required: true
      schema:
        type: integer
    StateFilter:
      name: state
      in: query
      schema:
        type: string
        enum: [pending, active, expiring, expired, rejected, revoked, superseded]
    DueFilter:
      name: due
      in: query
      schema:
        type: string
        enum: [all, expiring, expired]
  responses:
    Forbidden:
      description: Member lacks required capability
    NotFound:
      description: Workspace or tenant scope is not entitled
    ValidationError:
      description: Input or transition validation failed
  schemas:
    FindingExceptionResource:
      type: object
      required:
        - id
        - tenant_id
        - finding_id
        - status
        - validity_state
      properties:
        id:
          type: integer
        tenant_id:
          type: integer
        finding_id:
          type: integer
        status:
          type: string
          enum: [pending, active, expiring, expired, rejected, revoked, superseded]
        validity_state:
          type: string
          enum: [valid, expiring, expired, revoked, rejected, missing_support]
        owner_user_id:
          type: integer
          nullable: true
        requested_by_user_id:
          type: integer
        approved_by_user_id:
          type: integer
          nullable: true
        requested_at:
          type: string
          format: date-time
        approved_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        review_due_at:
          type: string
          format: date-time
          nullable: true
    FindingExceptionDetail:
      allOf:
        - $ref: '#/components/schemas/FindingExceptionResource'
        - type: object
          properties:
            request_reason:
              type: string
            decisions:
              type: array
              items:
                $ref: '#/components/schemas/FindingExceptionDecision'
            evidence_references:
              type: array
              items:
                $ref: '#/components/schemas/FindingExceptionEvidenceReference'
    FindingExceptionDecision:
      type: object
      required:
        - decision_type
        - actor_user_id
        - decided_at
      properties:
        decision_type:
          type: string
          enum: [requested, approved, rejected, renewal_requested, renewed, revoked]
        actor_user_id:
          type: integer
        reason:
          type: string
          nullable: true
        effective_from:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        decided_at:
          type: string
          format: date-time
    FindingExceptionEvidenceReference:
      type: object
      required:
        - source_type
        - label
      properties:
        source_type:
          type: string
        source_id:
          type: string
          nullable: true
        source_fingerprint:
          type: string
          nullable: true
        label:
          type: string
        measured_at:
          type: string
          format: date-time
          nullable: true
        summary_payload:
          type: object
          additionalProperties: true
          nullable: true
    RequestExceptionInput:
      type: object
      required:
        - owner_user_id
        - request_reason
        - review_due_at
      properties:
        owner_user_id:
          type: integer
        request_reason:
          type: string
          maxLength: 2000
        review_due_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          nullable: true
        evidence_references:
          type: array
          items:
            $ref: '#/components/schemas/FindingExceptionEvidenceReference'
    ApproveExceptionInput:
      type: object
      required:
        - effective_from
        - expires_at
      properties:
        effective_from:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        approval_reason:
          type: string
          nullable: true
          maxLength: 2000
    RejectExceptionInput:
      type: object
      required:
        - rejection_reason
      properties:
        rejection_reason:
          type: string
          maxLength: 2000
    RenewExceptionInput:
      type: object
      required:
        - request_reason
        - review_due_at
      properties:
        request_reason:
          type: string
          maxLength: 2000
        review_due_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          nullable: true
        evidence_references:
          type: array
          items:
            $ref: '#/components/schemas/FindingExceptionEvidenceReference'
    RevokeExceptionInput:
      type: object
      required:
        - revocation_reason
      properties:
        revocation_reason:
          type: string
          maxLength: 2000