openapi: 3.0.3
info:
  title: TokenTimer Core API Contract
  version: 0.8.2
  description: |
    TokenTimer Core API for authentication, workspaces, token lifecycle management,
    alerts, audit, and integration import/scan workflows.
    This contract is also used as a compatibility baseline across core, cloud, and enterprise variants.
servers:
  - url: /
    description: Same origin as this docs page (local, staging, or production)
tags:
  - name: Health
    description: Service health and readiness endpoints.
  - name: Auth
    description: Authentication and account verification flows.
  - name: Session
    description: Session and CSRF token endpoints.
  - name: Workspaces
    description: Workspace CRUD, membership, domains, and workspace-level settings.
  - name: Tokens
    description: Token CRUD, lifecycle actions, and duplicate/bulk handling.
  - name: Account
    description: User account profile, security, plan, and usage endpoints.
  - name: Audit
    description: Audit trail queries and activity reporting.
  - name: Alerts
    description: Alert rules, destinations, queue visibility, and preferences.
  - name: Integrations
    description: Integration scans, discovery, and import operations.
  - name: CertOps
    description: Certificate lifecycle orchestration (workspace-scoped). Control plane never stores private key material.
  - name: CertOps Agent
    description: Machine surface for the CertOps executor and outbound-only agent. Scoped-token authenticated.
security:
  - cookieAuth: []
paths:
  /health:
    get:
      tags: [Health]
      summary: Health endpoint
      responses:
        "200":
          description: Service health payload
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - timestamp
                properties:
                  status:
                    type: string
                    example: healthy
                  timestamp:
                    type: string
                    format: date-time
                  uptime:
                    type: number
                    format: double
                  memory:
                    type: object
                    additionalProperties: true
                  environment:
                    type: string
  /api/auth/features:
    get:
      tags: [Auth]
      summary: Auth feature availability flags
      responses:
        "200":
          description: Feature flags for auth providers
          content:
            application/json:
              schema:
                type: object
                required:
                  - saml
                  - oidc
                properties:
                  saml:
                    type: boolean
                  oidc:
                    type: boolean
  /api/session:
    get:
      tags: [Session]
      summary: Get current authenticated session
      operationId: getSession
      description: Returns session state and user profile when the request is authenticated.
      security: []
      responses:
        "200":
          description: Session state payload
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [loggedIn]
                    properties:
                      loggedIn:
                        type: boolean
                        enum: [false]
                  - type: object
                    required: [loggedIn, user]
                    properties:
                      loggedIn:
                        type: boolean
                        enum: [true]
                      user:
                        $ref: "#/components/schemas/SessionUser"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/csrf-token:
    get:
      tags: [Session]
      summary: Issue CSRF token
      operationId: getCsrfToken
      description: Issues a CSRF token that must be sent on state-changing requests.
      security: []
      responses:
        "200":
          description: CSRF token
          content:
            application/json:
              schema:
                type: object
                required: [csrfToken]
                properties:
                  csrfToken:
                    type: string
  /auth/login:
    post:
      tags: [Auth]
      summary: Login with email and password
      operationId: loginWithPassword
      description: Authenticates a user and returns either a session payload or a 2FA challenge.
      security: []
      requestBody:
        description: Credentials used for password authentication.
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Login success or 2FA challenge
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/LoginSuccessResponse"
                  - $ref: "#/components/schemas/LoginNeeds2FAResponse"
        "401":
          description: Invalid credentials or unverified account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/verify-2fa:
    post:
      tags: [Auth]
      summary: Verify 2FA token and finalize login
      operationId: verifyTwoFactorLogin
      description: Completes login for a user that has two-factor authentication enabled.
      security: []
      requestBody:
        description: One-time code generated by the configured authenticator app.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
      responses:
        "200":
          description: 2FA verification success
          content:
            application/json:
              schema:
                type: object
                required: [success]
                properties:
                  success:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/verify-email/{token}:
    get:
      tags: [Auth]
      summary: Verify account email token
      operationId: verifyEmailToken
      description: Validates an email verification token and redirects to the frontend.
      security: []
      parameters:
        - $ref: "#/components/parameters/tokenParam"
      responses:
        "302":
          description: Redirects to frontend login page with verification outcome
  /auth/resend-verification:
    post:
      tags: [Auth]
      summary: Resend verification email
      operationId: resendVerificationEmail
      description: Sends a new account verification email for the authenticated or provided user.
      security: []
      requestBody:
        description: Optional target email. If omitted, the current session user is used.
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Resend result
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  emailSent:
                    type: boolean
                  error:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/request-password-reset:
    post:
      tags: [Auth]
      summary: Request password reset email
      operationId: requestPasswordReset
      description: Starts the password reset flow by sending a reset link to the user email.
      security: []
      requestBody:
        description: Email address for the account that should receive the reset link.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Reset request accepted
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password using token
      operationId: resetPasswordWithToken
      description: Applies a new password using a previously issued password reset token.
      security: []
      requestBody:
        description: Reset token and replacement password.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, newPassword]
              properties:
                token:
                  type: string
                newPassword:
                  type: string
                  minLength: 12
                resetTwoFactor:
                  type: boolean
                  description: >-
                    When true, clears TOTP two-factor authentication after the
                    password is reset (ignored for Google SSO accounts).
      responses:
        "200":
          description: Password updated
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message:
                    type: string
                  twoFactorReset:
                    type: boolean
                    description: True when authenticator 2FA was cleared during reset.
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/logout:
    post:
      tags: [Auth]
      summary: Logout current session
      operationId: logoutCurrentSession
      description: Invalidates the current session cookie.
      responses:
        "200":
          description: Logout success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/logout:
    post:
      tags: [Auth]
      summary: Compatibility logout endpoint
      operationId: logoutCompatibility
      description: Legacy alias retained for client compatibility. Prefer `/api/logout` for new integrations.
      security: []
      responses:
        "200":
          description: Logout success
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
  /api/account/2fa/setup:
    post:
      tags: [Account]
      summary: Begin 2FA setup
      responses:
        "200":
          description: 2FA seed generated
          content:
            application/json:
              schema:
                type: object
                required: [secret, otpauth]
                properties:
                  secret:
                    type: string
                  otpauth:
                    type: string
                  qr:
                    type: string
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/2fa/enable:
    post:
      tags: [Account]
      summary: Enable 2FA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
      responses:
        "200":
          description: 2FA enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/2fa/disable:
    post:
      tags: [Account]
      summary: Disable 2FA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword]
              properties:
                currentPassword:
                  type: string
      responses:
        "200":
          description: 2FA disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/change-password:
    post:
      tags: [Account]
      summary: Change account password
      operationId: changeAccountPassword
      description: Changes the password for the currently authenticated account.
      requestBody:
        description: Current password and new password (minimum 12 characters).
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword:
                  type: string
                newPassword:
                  type: string
                  minLength: 12
      responses:
        "200":
          description: Password changed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces:
    get:
      tags: [Workspaces]
      summary: List workspaces for current user
      operationId: listWorkspaces
      description: Returns workspaces visible to the authenticated user with pagination metadata.
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Workspace list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Workspace"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Workspaces]
      summary: Create workspace
      operationId: createWorkspace
      description: Creates a new workspace and assigns the creator as an administrator.
      requestBody:
        description: Workspace display name.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "201":
          description: Workspace created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}:
    get:
      tags: [Workspaces]
      summary: Get workspace by id
      operationId: getWorkspaceById
      description: Returns details for a single workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Workspace details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Workspaces]
      summary: Update workspace
      operationId: updateWorkspace
      description: Updates mutable workspace fields.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        description: Workspace fields to update.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Workspace updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Workspaces]
      summary: Delete workspace
      operationId: deleteWorkspace
      description: Deletes a workspace if the caller has sufficient permissions.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "204":
          description: Workspace deleted
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/members:
    get:
      tags: [Workspaces]
      summary: List workspace members
      operationId: listWorkspaceMembers
      description: Lists members and invitation-backed members for the workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkspaceMember"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
    post:
      tags: [Workspaces]
      summary: Add or invite workspace member
      operationId: addWorkspaceMember
      description: Adds an existing user or creates/updates an invitation for the provided email.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        description: Target member email and role assignment.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email:
                  type: string
                  format: email
                role:
                  type: string
                  enum: [viewer, workspace_manager]
      responses:
        "201":
          description: Member created or invitation updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: integer
                    nullable: true
                  role:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Plan limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/members/{userId}:
    patch:
      tags: [Workspaces]
      summary: Update member role
      operationId: updateWorkspaceMemberRole
      description: Changes the role of an existing workspace member.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/userIdParam"
      requestBody:
        description: New role for the specified member.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  type: string
                  enum: [viewer, workspace_manager]
      responses:
        "200":
          description: Role updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: integer
                  role:
                    type: string
    delete:
      tags: [Workspaces]
      summary: Remove workspace member
      operationId: removeWorkspaceMember
      description: Removes a user from the workspace membership list.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/userIdParam"
      responses:
        "204":
          description: Member removed
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/invitations:
    get:
      tags: [Workspaces]
      summary: List pending workspace invitations
      operationId: listWorkspaceInvitations
      description: >
        Returns unaccepted invitations for the workspace. The invitation
        `token` is a bearer secret and is never returned by this endpoint.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 100
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Pending invitations
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        email:
                          type: string
                          format: email
                        role:
                          type: string
                        invited_by:
                          type: integer
                        created_at:
                          type: string
                          format: date-time
                        accepted_at:
                          type: string
                          format: date-time
                          nullable: true
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/invitations/{invitationId}:
    delete:
      tags: [Workspaces]
      summary: Cancel a pending workspace invitation
      operationId: cancelWorkspaceInvitation
      description: >
        Deletes a pending invitation (accepted_at IS NULL) and emits an
        INVITATION_CANCELLED audit event. Does not touch invitations that have
        already been accepted.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: invitationId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Invitation cancelled
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/transfer-tokens:
    post:
      tags: [Workspaces]
      summary: Transfer token ownership between workspaces
      operationId: transferWorkspaceTokens
      description: Moves tokens from one workspace to another workspace within the same account scope.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        description: Source workspace and token ids to move into the target workspace.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from_workspace_id, token_ids]
              properties:
                from_workspace_id:
                  type: string
                  format: uuid
                token_ids:
                  type: array
                  items:
                    type: integer
      responses:
        "200":
          description: Transfer result
          content:
            application/json:
              schema:
                type: object
                properties:
                  moved:
                    type: integer
                  targetWorkspaceId:
                    type: string
                    format: uuid
                  fromWorkspaceId:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "402":
          description: Transfer exceeds plan limits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/tokens:
    get:
      tags: [Tokens]
      summary: List tokens
      operationId: listTokens
      description: Lists tokens with optional workspace and text filtering.
      parameters:
        - in: query
          name: workspace_id
          schema:
            type: string
            format: uuid
        - in: query
          name: q
          schema:
            type: string
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Token list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Token"
                  total:
                    type: integer
                  facets:
                    type: object
                    additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Tokens]
      summary: Create token
      operationId: createToken
      description: Creates a token record. Can also resolve duplicate flows when confirmation flags are used.
      requestBody:
        description: Token payload including workspace id, token name, type, and optional metadata.
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TokenCreateRequest"
      responses:
        "201":
          description: Token created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Token"
        "200":
          description: Duplicate confirm path updated existing token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Token"
        "400":
          $ref: "#/components/responses/BadRequest"
        "402":
          description: Plan token limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Duplicate token candidate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/tokens/{id}:
    get:
      tags: [Tokens]
      summary: Get token by id
      operationId: getTokenById
      description: Fetches a single token by identifier.
      parameters:
        - $ref: "#/components/parameters/tokenIdParam"
      responses:
        "200":
          description: Token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Token"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    put:
      tags: [Tokens]
      summary: Update token by id
      operationId: updateTokenById
      description: Updates mutable fields of an existing token.
      parameters:
        - $ref: "#/components/parameters/tokenIdParam"
      requestBody:
        description: Partial token fields to update.
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Token"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Managed-backed certificate token must be retired before reclassification
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Tokens]
      summary: Delete token by id
      operationId: deleteTokenById
      description: Deletes a token record.
      parameters:
        - $ref: "#/components/parameters/tokenIdParam"
      responses:
        "200":
          description: Token deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Managed-backed certificate token must be retired before removal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/tokens/bulk:
    put:
      tags: [Tokens]
      summary: Bulk update tokens
      operationId: bulkUpdateTokens
      description: Updates multiple tokens in one request (supports section assignment) and returns per-item processing details.
      requestBody:
        description: List of token ids and fields to update.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, section]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                section:
                  nullable: true
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
      responses:
        "200":
          description: Bulk update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  successCount:
                    type: integer
                  failedCount:
                    type: integer
                  results:
                    type: object
                    additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Tokens]
      summary: Bulk delete tokens
      operationId: bulkDeleteTokens
      description: Deletes multiple tokens in one request and returns per-item processing details.
      requestBody:
        description: List of token ids to delete.
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
      responses:
        "200":
          description: Bulk delete result
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  successCount:
                    type: integer
                  failedCount:
                    type: integer
                  results:
                    type: object
                    additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/export:
    get:
      tags: [Account]
      summary: Export account and workspace data
      parameters:
        - in: query
          name: scope
          schema:
            type: string
            enum: [user, workspace, organization]
        - in: query
          name: workspace_id
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: JSON export attachment
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/export-audit:
    get:
      tags: [Audit]
      summary: Export audit events
      parameters:
        - in: query
          name: scope
          schema:
            type: string
            enum: [user, workspace, organization]
        - in: query
          name: workspace_id
          schema:
            type: string
            format: uuid
        - in: query
          name: format
          schema:
            type: string
            enum: [json, csv]
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/auditSinceParam"
        - $ref: "#/components/parameters/auditUntilParam"
      responses:
        "200":
          description: Audit export payload or CSV
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
            text/csv:
              schema:
                type: string
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/plan:
    get:
      tags: [Account]
      summary: Account and plan usage counters
      parameters:
        - in: query
          name: workspace_id
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Usage counters and limits
          content:
            application/json:
              schema:
                type: object
                required:
                  [
                    plan,
                    tokenCount,
                    tokenLimit,
                    alertUsageMonth,
                    alertLimitMonth,
                  ]
                properties:
                  plan:
                    type: string
                  tokenCount:
                    type: integer
                  tokenLimit:
                    oneOf:
                      - type: integer
                      - type: number
                  alertUsageMonth:
                    type: integer
                  alertLimitMonth:
                    oneOf:
                      - type: integer
                      - type: number
                  memberCount:
                    type: integer
                  memberLimit:
                    oneOf:
                      - type: integer
                      - type: number
                  workspaceCount:
                    type: integer
                  workspaceLimit:
                    oneOf:
                      - type: integer
                      - type: number
                  integrationScansUsed:
                    type: integer
                    nullable: true
                  integrationScansLimit:
                    type: integer
                    nullable: true
                  integrationScansRemaining:
                    type: integer
                    nullable: true
        "500":
          $ref: "#/components/responses/InternalError"
  /api/organization/usage:
    get:
      tags: [Account]
      summary: Organization usage summary
      responses:
        "200":
          description: Current month usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  monthUsage:
                    type: integer
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/audit-events:
    get:
      tags: [Audit]
      summary: List audit events
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
        - in: query
          name: scope
          schema:
            type: string
            enum: [organization]
        - in: query
          name: action
          schema:
            type: string
        - in: query
          name: query
          schema:
            type: string
        - $ref: "#/components/parameters/auditSinceParam"
        - $ref: "#/components/parameters/auditUntilParam"
      responses:
        "200":
          description: Audit event list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AuditEvent"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/audit-events:
    get:
      tags: [Audit]
      summary: List workspace scoped audit events
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
        - $ref: "#/components/parameters/auditSinceParam"
        - $ref: "#/components/parameters/auditUntilParam"
      responses:
        "200":
          description: Workspace audit event list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AuditEvent"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/audit:
    get:
      tags: [Audit]
      summary: Legacy workspace audit feed
      parameters:
        - in: query
          name: workspace_id
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: event_type
          schema:
            type: string
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Audit list and pagination
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/alert-queue/requeue:
    post:
      tags: [Alerts]
      summary: Bulk requeue pending alerts
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                workspace_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Requeue result
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/alert-queue/{id}/retry:
    post:
      tags: [Alerts]
      summary: Retry a specific alert delivery
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                channel:
                  type: string
      responses:
        "200":
          description: Retry queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          description: Retry cooldown active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/alert-queue:
    get:
      tags: [Alerts]
      summary: List alert queue entries
      responses:
        "200":
          description: Alert queue list
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
  /api/alert-stats:
    get:
      tags: [Alerts]
      summary: Get alert queue statistics
      responses:
        "200":
          description: Alert stats
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/vault/scan:
    post:
      tags: [Integrations]
      summary: Scan Vault for importable credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/vault/mounts:
    post:
      tags: [Integrations]
      summary: List available Vault mounts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultMountsRequest"
      responses:
        "200":
          description: Vault mount list
          content:
            application/json:
              schema:
                type: object
                required: [mounts]
                properties:
                  mounts:
                    type: array
                    items:
                      type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/vault/import:
    post:
      tags: [Integrations]
      summary: Import selected Vault findings as tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationImportRequest"
      responses:
        "200":
          description: Import result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationImportResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/gitlab/scan:
    post:
      tags: [Integrations]
      summary: Scan GitLab credentials and expirations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/github/scan:
    post:
      tags: [Integrations]
      summary: Scan GitHub credentials and expirations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/aws/detect-regions:
    post:
      tags: [Integrations]
      summary: Detect AWS regions from credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Region detection result
          content:
            application/json:
              schema:
                type: object
                required: [regions]
                properties:
                  regions:
                    type: array
                    items:
                      type: string
  /api/v1/integrations/aws/scan:
    post:
      tags: [Integrations]
      summary: Scan AWS resources for token-like credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/azure/scan:
    post:
      tags: [Integrations]
      summary: Scan Azure resources for credentials and expirations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/gcp/scan:
    post:
      tags: [Integrations]
      summary: Scan GCP resources for credentials and expirations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/azure-ad/scan:
    post:
      tags: [Integrations]
      summary: Scan Azure AD apps and credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderScanRequest"
      responses:
        "200":
          description: Integration scan result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationScanResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/integrations/check-duplicates:
    post:
      tags: [Integrations]
      summary: Check duplicate candidates before import
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: Duplicate analysis result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /api/v1/integrations/import:
    post:
      tags: [Integrations]
      summary: Generic import endpoint for integration findings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationImportRequest"
      responses:
        "200":
          description: Import result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationImportResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account:
    delete:
      tags: [Account]
      summary: Delete authenticated account
      operationId: deleteAccount
      description: Deletes or anonymizes the authenticated account, with ownership safety checks.
      parameters:
        - in: query
          name: force
          schema:
            type: boolean
      responses:
        "200":
          description: Account deletion completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        "409":
          description: Deletion blocked because user is sole admin in one or more workspaces
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/admin/users/{userId}/system-admin:
    patch:
      tags: [Account]
      summary: Grant or revoke system admin
      operationId: setSystemAdmin
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Sets installation-wide system admin (`users.is_admin`) for a user. Requires an authenticated system admin. The last system admin cannot demote themselves. On TokenTimer Cloud this route does not exist; TokenTimer operates the instance.
      parameters:
        - $ref: "#/components/parameters/userIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [is_admin]
              properties:
                is_admin:
                  type: boolean
      responses:
        "200":
          description: System admin flag updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: integer
                  is_admin:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/admin/system-settings:
    get:
      tags: [Account]
      summary: Get system settings
      operationId: getSystemSettings
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Returns SMTP and WhatsApp system settings for admin users. On TokenTimer Cloud this route does not exist; TokenTimer operates SMTP and WhatsApp delivery.
      responses:
        "200":
          description: System settings
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
    put:
      tags: [Account]
      summary: Update system settings
      operationId: updateSystemSettings
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Updates SMTP and WhatsApp configuration values for admin users. On TokenTimer Cloud this route does not exist; TokenTimer operates SMTP and WhatsApp delivery.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated system settings
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
  /api/admin/test-smtp:
    post:
      tags: [Account]
      summary: Send SMTP test email
      operationId: testSmtpConfiguration
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Sends a test email using current SMTP settings. On TokenTimer Cloud this route does not exist; TokenTimer operates SMTP delivery.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: SMTP test result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/admin/test-whatsapp:
    post:
      tags: [Account]
      summary: Send WhatsApp test message as admin
      operationId: testAdminWhatsAppConfiguration
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Sends a test WhatsApp message using current system settings. On TokenTimer Cloud this route does not exist; TokenTimer operates WhatsApp delivery.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone:
                  type: string
      responses:
        "200":
          description: WhatsApp test result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/test-webhook:
    post:
      tags: [Alerts]
      summary: Send test webhook
      operationId: sendTestWebhook
      description: Sends a test webhook payload to validate destination settings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Webhook test result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/test-whatsapp:
    post:
      tags: [Alerts]
      summary: Send workspace WhatsApp test message
      operationId: sendWorkspaceWhatsAppTest
      description: Sends a workspace-scoped WhatsApp test message with cooldown and validation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspace_id:
                  type: string
                  format: uuid
                phone_e164:
                  type: string
      responses:
        "200":
          description: WhatsApp test result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          description: Cooldown active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /webhooks/twilio/whatsapp/status:
    post:
      tags: [Alerts]
      summary: Twilio WhatsApp delivery status webhook
      operationId: twilioWhatsAppStatusWebhook
      description: Receives Twilio status callbacks and updates WhatsApp delivery metadata.
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Webhook processed
  /auth/register:
    post:
      tags: [Auth]
      summary: Register user from invitation
      operationId: registerFromInvitation
      description: Creates a user account using a valid invitation token.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, email, password]
              properties:
                token:
                  type: string
                email:
                  type: string
                  format: email
                password:
                  type: string
                first_name:
                  type: string
                last_name:
                  type: string
      responses:
        "200":
          description: Registration success
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: Invitation or account conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/verify-email:
    get:
      tags: [Auth]
      summary: Verify email endpoint without token
      operationId: verifyEmailMissingToken
      description: Compatibility route returning an explicit error when token is missing.
      security: []
      responses:
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{id}/auto-sync:
    get:
      tags: [Workspaces]
      summary: List auto-sync configs
      operationId: listAutoSyncConfigs
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Lists auto-sync configurations for a workspace. TokenTimer Cloud does not offer auto-sync (this route returns 404); credentials are never stored there.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Auto-sync config list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Workspaces]
      summary: Create auto-sync config
      operationId: createAutoSyncConfig
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Creates a new provider auto-sync configuration for a workspace. TokenTimer Cloud does not offer auto-sync (this route returns 404); credentials are never stored there.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, credentials]
              properties:
                provider:
                  type: string
                credentials:
                  type: object
                  additionalProperties: true
                scan_params:
                  type: object
                  additionalProperties: true
                frequency:
                  type: string
                schedule_time:
                  type: string
                schedule_tz:
                  type: string
      responses:
        "201":
          description: Auto-sync config created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Config already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/auto-sync/{configId}:
    put:
      tags: [Workspaces]
      summary: Update auto-sync config
      operationId: updateAutoSyncConfig
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Updates an existing auto-sync configuration. TokenTimer Cloud does not offer auto-sync (this route returns 404).
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: configId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated auto-sync config
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Workspaces]
      summary: Delete auto-sync config
      operationId: deleteAutoSyncConfig
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Deletes an auto-sync configuration from a workspace (also deletes stored credentials). TokenTimer Cloud does not offer auto-sync (this route returns 404).
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: configId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Auto-sync config deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/auto-sync/{configId}/run:
    post:
      tags: [Workspaces]
      summary: Trigger auto-sync run
      operationId: triggerAutoSyncRun
      description: <span class="tt-deployment-badge tt-deployment-badge--selfhosted">Self-hosted / Enterprise only</span> Queues an immediate run for an enabled auto-sync configuration. TokenTimer Cloud does not offer auto-sync (this route returns 404).
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: configId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sync queued
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/control-center/stats:
    get:
      tags: [Workspaces]
      summary: Control Center dashboard stats
      operationId: getControlCenterStats
      description: >-
        Returns aggregated Control Center statistics for the workspace dashboard.
        Requires workspace admin or workspace manager role.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Control Center stats payload
          content:
            application/json:
              schema:
                type: object
                required:
                  - totalAssets
                  - buckets
                  - sources
                  - needsAttention
                  - neverExpires
                  - privilegeHighlights
                  - privilegeHighlightsTotal
                  - autoSync
                  - generatedAt
                  - isComplete
                properties:
                  totalAssets:
                    type: integer
                  buckets:
                    type: object
                    additionalProperties:
                      type: integer
                  sources:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        name:
                          type: string
                        count:
                          type: integer
                  needsAttention:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  neverExpires:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                    description: >-
                      First page of perpetual (never-expiring) assets. Use
                      GET .../control-center/never-expires with an offset to
                      load additional pages.
                  neverExpiresHasMore:
                    type: boolean
                    description: True if more perpetual assets exist beyond this first page.
                  privilegeHighlights:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                    description: >-
                      First page of the scopes/privileges highlight list. Use
                      GET .../control-center/privilege-highlights with an
                      offset to load additional pages.
                  privilegeHighlightsTotal:
                    type: integer
                    description: >-
                      Total count of tokens with recorded scopes/privileges in the
                      workspace. privilegeHighlights is a capped preview list
                      (top matches only), so use this field for the headline count.
                  privilegeHighlightsHasMore:
                    type: boolean
                    description: True if more privilege highlights exist beyond this first page.
                  autoSync:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  generatedAt:
                    type: string
                    format: date-time
                  isComplete:
                    type: boolean
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/control-center/never-expires:
    get:
      tags: [Workspaces]
      summary: Paginated perpetual (never-expiring) assets
      operationId: getControlCenterNeverExpires
      description: >-
        Returns a page of perpetual assets for infinite-scroll loading in the
        Control Center. Requires workspace admin or workspace manager role.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
            minimum: 0
      responses:
        "200":
          description: A page of perpetual assets
          content:
            application/json:
              schema:
                type: object
                required: [items, total, hasMore]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  total:
                    type: integer
                  hasMore:
                    type: boolean
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/control-center/privilege-highlights:
    get:
      tags: [Workspaces]
      summary: Paginated scopes/privileges highlight list
      operationId: getControlCenterPrivilegeHighlights
      description: >-
        Returns a page of the scopes/privileges highlight list (sorted by
        privilege risk score) for infinite-scroll loading in the Control
        Center. Requires workspace admin or workspace manager role.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
            minimum: 0
      responses:
        "200":
          description: A page of privilege highlights
          content:
            application/json:
              schema:
                type: object
                required: [items, total, hasMore]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  total:
                    type: integer
                  hasMore:
                    type: boolean
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/contacts:
    get:
      tags: [Workspaces]
      summary: List workspace contacts
      operationId: listWorkspaceContacts
      description: Returns contact records for the workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Contact list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Workspaces]
      summary: Create workspace contact
      operationId: createWorkspaceContact
      description: Creates a workspace contact with email and/or phone details.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [first_name, last_name]
              properties:
                first_name:
                  type: string
                last_name:
                  type: string
                phone_e164:
                  type: string
                details:
                  type: object
                  additionalProperties: true
      responses:
        "201":
          description: Contact created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/contacts/{contactId}:
    put:
      tags: [Workspaces]
      summary: Update workspace contact
      operationId: updateWorkspaceContact
      description: Updates mutable fields of a workspace contact.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: contactId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Contact updated
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Workspaces]
      summary: Delete workspace contact
      operationId: deleteWorkspaceContact
      description: Deletes a contact from the workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: contactId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Contact deleted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/domains:
    get:
      tags: [Workspaces]
      summary: List endpoint monitors
      operationId: listWorkspaceDomainMonitors
      description: Lists endpoint monitoring configurations for a workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Endpoint monitor list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Workspaces]
      summary: Create endpoint monitor
      operationId: createWorkspaceDomainMonitor
      description: Creates a domain monitor and optionally links an auto-created token.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                health_check_enabled:
                  type: boolean
                check_interval:
                  type: string
                alert_after_failures:
                  type: integer
                contact_group_id:
                  type: string
      responses:
        "201":
          description: Endpoint monitor created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/domains/{domainId}:
    put:
      tags: [Workspaces]
      summary: Update endpoint monitor
      operationId: updateWorkspaceDomainMonitor
      description: Updates endpoint monitor settings such as check interval and enablement.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: domainId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Endpoint monitor updated
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Workspaces]
      summary: Delete endpoint monitor
      operationId: deleteWorkspaceDomainMonitor
      description: Removes a domain monitor from the workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: domainId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Endpoint monitor deleted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/domains/{domainId}/check:
    post:
      tags: [Workspaces]
      summary: Run endpoint health check now
      operationId: runWorkspaceDomainMonitorCheck
      description: Executes an immediate health check for a monitored endpoint.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: domainId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Health check result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/domain-checker/lookup:
    post:
      tags: [Workspaces]
      summary: Discover subdomains for a root domain
      operationId: workspaceDomainCheckerLookup
      description: |
        Runs passive subdomain discovery with subfinder's default behavior for publicly known hostnames under the given root domain.
        It does not scan private DNS or internal network records. Results may be slow, partial, or capped; clients should use a long HTTP timeout.
        Lookup requests are rate limited per workspace and user.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Root domain to search (for example example.com)
                  example: example.com
      responses:
        "200":
          description: Passive subdomain discovery results
          content:
            application/json:
              schema:
                type: object
                required: [domain, source, items]
                properties:
                  domain:
                    type: string
                  source:
                    type: string
                    example: subfinder
                  items:
                    type: array
                    description: Hostname findings. The field name is retained for compatibility with existing domain-checker clients.
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                          description: Discovered hostname.
                        domains:
                          type: array
                          items:
                            type: string
                        sources:
                          type: array
                          items:
                            type: string
                            enum: [subfinder]
                        checked:
                          type: boolean
                      additionalProperties: true
                  partial:
                    type: boolean
                    description: True when one discovery tool failed or results were capped.
                  toolErrors:
                    type: array
                    items:
                      type: object
                      properties:
                        tool:
                          type: string
                          enum: [subfinder]
                        code:
                          type: string
                        message:
                          type: string
                  meta:
                    type: object
                    properties:
                      tools:
                        type: array
                        items:
                          type: string
                          enum: [subfinder]
                      toolsSucceeded:
                        type: integer
                      toolsFailed:
                        type: integer
                      truncated:
                        type: boolean
                      durationMs:
                        type: integer
                      maxResults:
                        type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: A lookup is already running for this workspace and domain
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    example: DOMAIN_CHECKER_BUSY
        "429":
          description: Domain checker lookup rate limited
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    example: DOMAIN_CHECKER_RATE_LIMITED
        "502":
          description: Discovery tool error or invalid response
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
        "503":
          description: Discovery tools unavailable or all sources failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    example: DOMAIN_CHECKER_FAILED
                  toolErrors:
                    type: array
                    items:
                      type: object
                      properties:
                        tool:
                          type: string
                          enum: [subfinder]
                        code:
                          type: string
                        message:
                          type: string
        "504":
          description: Domain discovery lookup timed out
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    example: DOMAIN_CHECKER_TIMEOUT
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/domain-checker/import:
    post:
      tags: [Workspaces]
      summary: Import selected hostnames from domain checker results
      operationId: workspaceDomainCheckerImport
      description: |
        Fetches the live TLS certificate for each selected hostname, then creates ssl_cert tokens in the workspace.
        Duplicate imports are skipped when a matching token already exists.
        Optionally creates endpoint monitors (`domain_monitors`) for each discovered hostname when
        `monitorOptions.enabled` is true. Existing monitors for the same workspace and URL are left untouched.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain, certificates]
              properties:
                domain:
                  type: string
                  description: Root domain that was used for lookup
                  example: example.com
                certificates:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    type: object
                    additionalProperties: true
                monitorOptions:
                  type: object
                  description: Optional block enabling bulk creation of endpoint monitors for the imported hostnames.
                  properties:
                    enabled:
                      type: boolean
                      description: Whether to create `domain_monitors` rows for each imported hostname.
                      default: false
                    health_check_enabled:
                      type: boolean
                      description: Enable periodic HTTP(S) health checks on the created monitors.
                      default: false
                    check_interval:
                      type: string
                      description: Health check cadence for the created monitors.
                      enum: ["1min", "5min", "30min", "hourly", "daily"]
                      default: "hourly"
                    alert_after_failures:
                      type: integer
                      description: Number of consecutive failures before a "down" alert is triggered.
                      minimum: 1
                      maximum: 10
                      default: 2
                    contact_group_id:
                      type: string
                      nullable: true
                      description: Contact group for alerts from the created monitors. Falls back to the workspace default if omitted.
      responses:
        "201":
          description: Import result with created and skipped rows
          content:
            application/json:
              schema:
                type: object
                required: [imported, skipped]
                properties:
                  imported:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      required: [tokenId]
                      properties:
                        certificateId:
                          type: string
                          nullable: true
                          description: Source certificate identifier from the domain checker payload when available.
                        tokenId:
                          type: integer
                          minimum: 1
                          description: Actual non-secret tokens.id value created or linked for the imported certificate.
                        name:
                          type: string
                        expiration:
                          type: string
                          format: date
                  skipped:
                    type: array
                    description: |-
                      Hostnames that were not imported. Each entry includes a `reason`
                      (`duplicate` or `invalid_certificate`) and a machine-readable `detail`
                      plus identifying info (hostnames, subject, expiration when available) so clients can
                      explain why the rows were skipped.
                    items:
                      type: object
                      additionalProperties: true
                      properties:
                        reason:
                          type: string
                          enum: [duplicate, invalid_certificate]
                        detail:
                          type: string
                          description: Machine-readable sub-reason.
                  skippedCounts:
                    type: object
                    description: Per-reason skip counts mirroring the `skipped` array.
                    properties:
                      duplicate:
                        type: integer
                      invalid:
                        type: integer
                  monitors:
                    type: object
                    description: Endpoint monitor creation summary when `monitorOptions.enabled` was true.
                    properties:
                      created:
                        type: integer
                        description: New monitors created for imported hostnames.
                      existing:
                        type: integer
                        description: Hostnames that already had a monitor in this workspace.
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/tokens/reassign-contact-group:
    post:
      tags: [Workspaces]
      summary: Reassign tokens between contact groups
      operationId: reassignTokenContactGroup
      description: Moves token contact_group_id values from one group to another in the workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from_group_id, to_group_id]
              properties:
                from_group_id:
                  type: string
                to_group_id:
                  type: string
      responses:
        "200":
          description: Reassignment result
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/notifications:
    get:
      tags: [Alerts]
      summary: List workspace operational notifications
      operationId: listWorkspaceNotifications
      description: >-
        Returns operational notifications for the dashboard bell, including
        auto-sync failures and alerts waiting for the delivery window.
        Workspace admins and managers receive provider-specific items; other
        members receive an empty list.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: Notification list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        kind:
                          type: string
                          enum: [error, warning, info]
                        text:
                          type: string
                        href:
                          type: string
                          nullable: true
                        provider:
                          type: string
                        count:
                          type: integer
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/alert-settings:
    put:
      tags: [Alerts]
      summary: Update workspace alert settings
      operationId: updateWorkspaceAlertSettings
      description: Updates workspace-level alert thresholds, channels, windows, and contact groups.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Alert settings update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/account/alert-settings:
    put:
      tags: [Alerts]
      summary: Update user alert settings
      operationId: updateUserAlertSettings
      description: Updates authenticated user alert thresholds and channel preferences.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Alert settings updated
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
  # ----------------------------------------------------------------------------
  # CertOps routes. M1 inventory routes are implemented with precise public-only
  # schemas below. M2/M4 machine, token, job, and agent namespaces remain frozen
  # skeletons so downstream overlays can build without route churn.
  # ----------------------------------------------------------------------------
  /api/v1/workspaces/{id}/certops/certificates:
    get:
      tags: [CertOps]
      summary: List managed certificates
      operationId: listCertOpsCertificates
      description: Returns the workspace's managed certificate inventory. Response objects contain public certificate material and public metadata only.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Managed certificate inventory
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsManagedCertificateListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: CertOps is disabled for this deployment or workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Endpoint not found
                code: NOT_FOUND
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [CertOps]
      summary: Create or register a managed certificate
      operationId: createCertOpsCertificate
      description: Registers a managed certificate from public certificate material. Request bodies containing private key material are rejected before feature gating.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsCertificateWriteRequest"
      responses:
        "201":
          description: Managed certificate registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsCertificateWriteResponse"
        "400":
          description: Missing certificate material, malformed public certificate input, or invalid key reference
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                missingCertificate:
                  value:
                    error: certificatePem is required
                    code: CERTOPS_CERTIFICATE_PEM_REQUIRED
                parseFailed:
                  value:
                    error: Certificate input could not be parsed
                    code: CERTOPS_CERTIFICATE_PARSE_FAILED
                invalidKeyReference:
                  value:
                    error: keyReference must be a non-secret reference
                    code: CERTOPS_KEY_REFERENCE_INVALID
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Authenticated user lacks the workspace manager/admin role required for CertOps writes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Forbidden: insufficient role"
                code: INSUFFICIENT_ROLE
        "404":
          description: CertOps is disabled for this deployment or workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Endpoint not found
                code: NOT_FOUND
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/certificates/{certId}:
    get:
      tags: [CertOps]
      summary: Get a managed certificate
      operationId: getCertOpsCertificate
      description: Returns a single managed certificate. Response objects contain public certificate material and public metadata only.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: certId
          required: true
          schema:
            type: string
          description: Managed certificate identifier.
      responses:
        "200":
          description: Managed certificate detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsManagedCertificateDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: CertOps is disabled or the certificate was not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                certopsDisabled:
                  value:
                    error: Endpoint not found
                    code: NOT_FOUND
                certificateNotFound:
                  value:
                    error: Certificate not found
                    code: CERTOPS_CERTIFICATE_NOT_FOUND
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/certificates/{certId}/instances:
    get:
      tags: [CertOps]
      summary: List managed certificate instances
      operationId: listCertOpsCertificateInstances
      description: Returns public, workspace-scoped certificate observation history for one managed certificate. Private key material, evidence, raw output, and secret fields are never returned.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: certId
          required: true
          schema:
            type: string
            format: uuid
          description: Managed certificate identifier.
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/offsetParam"
      responses:
        "200":
          description: Managed certificate instance history
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [items, pagination]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - workspaceId
                        - managedCertificateId
                        - targetId
                        - status
                        - source
                      properties:
                        id:
                          type: string
                          format: uuid
                        workspaceId:
                          type: string
                          format: uuid
                        managedCertificateId:
                          type: string
                          format: uuid
                        targetId:
                          type: string
                          format: uuid
                        domainMonitorId:
                          type: string
                          format: uuid
                          nullable: true
                        tokenId:
                          type: integer
                          nullable: true
                        status:
                          type: string
                        source:
                          type: string
                        sourceRef:
                          type: string
                          nullable: true
                        observedFingerprintSha256:
                          type: string
                          pattern: "^[a-f0-9]{64}$"
                          nullable: true
                        observedSerialNumber:
                          type: string
                          nullable: true
                        observedSubject:
                          type: string
                          nullable: true
                        observedIssuer:
                          type: string
                          nullable: true
                        observedNotBefore:
                          type: string
                          format: date-time
                          nullable: true
                        observedNotAfter:
                          type: string
                          format: date-time
                          nullable: true
                        deploymentReference:
                          type: string
                          nullable: true
                        observedAt:
                          type: string
                          format: date-time
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                          nullable: true
                        updatedAt:
                          type: string
                          format: date-time
                          nullable: true
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Managed certificate not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Certificate not found
                code: CERTOPS_CERTIFICATE_NOT_FOUND
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/certificates/{certId}/retire:
    post:
      tags: [CertOps]
      summary: Retire a managed certificate
      operationId: retireCertOpsCertificate
      description: Soft-retires a managed certificate by moving public lifecycle status to revoked or decommissioned. The managed certificate, linked token, and instance history are preserved; request bodies containing private key material are rejected before feature gating.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: certId
          required: true
          schema:
            type: string
            format: uuid
          description: Managed certificate identifier.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsCertificateRetireRequest"
      responses:
        "200":
          description: Managed certificate retired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsManagedCertificateDetailResponse"
        "400":
          description: Invalid retire request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidStatus:
                  value:
                    error: Invalid certificate retire status
                    code: CERTOPS_CERTIFICATE_RETIRE_STATUS_INVALID
                invalidReason:
                  value:
                    error: Invalid certificate retire reason
                    code: CERTOPS_CERTIFICATE_RETIRE_REASON_INVALID
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Authenticated user lacks the workspace manager/admin role required for CertOps writes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Forbidden: insufficient role"
                code: INSUFFICIENT_ROLE
        "404":
          description: CertOps is disabled or the certificate was not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                certopsDisabled:
                  value:
                    error: Endpoint not found
                    code: NOT_FOUND
                certificateNotFound:
                  value:
                    error: Certificate not found
                    code: CERTOPS_CERTIFICATE_NOT_FOUND
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/imports:
    post:
      tags: [CertOps]
      summary: Import certificates from public material
      operationId: createCertOpsImport
      description: Imports certificates from PEM/public material. Private key material is rejected before feature gating.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsCertificateWriteRequest"
      responses:
        "202":
          description: Import accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsCertificateWriteResponse"
        "400":
          description: Missing certificate material, malformed public certificate input, or invalid key reference
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                missingCertificate:
                  value:
                    error: certificatePem is required
                    code: CERTOPS_CERTIFICATE_PEM_REQUIRED
                parseFailed:
                  value:
                    error: Certificate input could not be parsed
                    code: CERTOPS_CERTIFICATE_PARSE_FAILED
                invalidKeyReference:
                  value:
                    error: keyReference must be a non-secret reference
                    code: CERTOPS_KEY_REFERENCE_INVALID
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Authenticated user lacks the workspace manager/admin role required for CertOps writes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Forbidden: insufficient role"
                code: INSUFFICIENT_ROLE
        "404":
          description: CertOps is disabled for this deployment or workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Endpoint not found
                code: NOT_FOUND
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/jobs:
    get:
      tags: [CertOps]
      summary: List CertOps jobs
      operationId: listCertOpsJobs
      description: >-
        Returns paginated public CertOps job summaries for the workspace. Consumers
        must follow pagination.limit and pagination.offset; long job histories can
        require multiple requests. Private key material, raw executor output, token
        hashes, and credentials are never returned.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - $ref: "#/components/parameters/certOpsReadLimitParam"
        - $ref: "#/components/parameters/certOpsReadOffsetParam"
        - in: query
          name: status
          schema:
            type: string
            enum: [pending_approval, approved, rejected, pending, claimed, running, succeeded, failed, blocked, cancelled]
        - in: query
          name: operation
          schema:
            type: string
            enum: [renew, deploy, reload, revoke, noop]
        - in: query
          name: source
          schema:
            type: string
            enum: [api, executor, system, automation, domain-monitor, endpoint-monitor, control-plane, external]
        - in: query
          name: subjectType
          schema:
            type: string
            enum: [managed_certificate, certificate_instance, certificate_target, token, domain, endpoint, external]
        - in: query
          name: subjectId
          schema:
            type: string
            maxLength: 128
      responses:
        "200":
          description: Job list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsJobListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          description: A filter contained private-key or forbidden secret material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [CertOps]
      summary: Create a manual CertOps job
      operationId: createCertOpsJob
      description: >-
        Creates a workspace-scoped CertOps job through the manual creation
        surface. This is a session-authenticated exception path for the
        pre-certops-scheduler window (and a break-glass path afterward);
        jobs created this way are always recorded with source "api" and
        cannot be attributed to the executor or automated-scheduler
        pipelines. Requires workspace_manager role or above. Private key or
        secret material in the request body is rejected.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsJobCreateRequest"
      responses:
        "201":
          description: Job created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsJobDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: idempotencyKey was already used with a different job request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Idempotency key was already used with a different CertOps job request
                code: CERTOPS_JOB_IDEMPOTENCY_CONFLICT
        "422":
          description: The request contained private-key or forbidden secret material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/jobs/{jobId}:
    get:
      tags: [CertOps]
      summary: Get a CertOps job
      operationId: getCertOpsJob
      description: Returns one sanitized CertOps job detail for the authorized workspace.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: jobId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Job detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsJobDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Job not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Certificate job not found
                code: CERTOPS_JOB_NOT_FOUND
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/jobs/{jobId}/log:
    get:
      tags: [CertOps]
      summary: List CertOps job log entries
      operationId: listCertOpsJobLog
      description: >-
        Returns paginated sanitized timeline entries for one workspace-scoped
        CertOps job. Consumers must follow pagination.limit and pagination.offset;
        long job histories can require multiple requests.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: jobId
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/certOpsReadLimitParam"
        - $ref: "#/components/parameters/certOpsReadOffsetParam"
      responses:
        "200":
          description: Job log list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsJobLogListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Job not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Certificate job not found
                code: CERTOPS_JOB_NOT_FOUND
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/jobs/{jobId}/evidence:
    get:
      tags: [CertOps]
      summary: List CertOps job evidence
      operationId: listCertOpsJobEvidence
      description: >-
        Returns paginated sanitized public evidence metadata for one
        workspace-scoped CertOps job. Consumers must follow pagination.limit and
        pagination.offset; long job histories can require multiple requests. Raw
        secrets, private keys, token hashes, and key-bearing artifacts are never
        returned.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: jobId
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/certOpsReadLimitParam"
        - $ref: "#/components/parameters/certOpsReadOffsetParam"
      responses:
        "200":
          description: Job evidence list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsEvidenceListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Job not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Certificate job not found
                code: CERTOPS_JOB_NOT_FOUND
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/tokens:
    get:
      tags: [CertOps]
      summary: List CertOps API tokens
      operationId: listCertOpsTokens
      description: Returns workspace-scoped CertOps API-token metadata only. Plaintext tokens, token hashes, Authorization headers, and credential material are never returned.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      responses:
        "200":
          description: API token metadata list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsApiTokenListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [CertOps]
      summary: Issue a scoped CertOps API token
      operationId: createCertOpsToken
      description: Issues a scoped CertOps API token. The one-time plaintext token is returned only by this create response and is never stored in recoverable form. List and revoke responses never include plaintext tokens, token hashes, Authorization headers, or credential material.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsApiTokenCreateRequest"
      responses:
        "201":
          description: API token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsApiTokenCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/workspaces/{id}/certops/tokens/{tokenId}/revoke:
    post:
      tags: [CertOps]
      summary: Revoke a scoped CertOps API token
      operationId: revokeCertOpsToken
      description: Revokes a scoped CertOps API token within the authorized workspace and returns metadata only. The response never includes plaintext tokens, token hashes, Authorization headers, or credential material.
      parameters:
        - $ref: "#/components/parameters/workspaceIdParam"
        - in: path
          name: tokenId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: API token revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsApiTokenRevokeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: CertOps is disabled or the API token was not found in this workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                certopsDisabled:
                  value:
                    error: Endpoint not found
                    code: NOT_FOUND
                tokenNotFound:
                  value:
                    error: CertOps API token not found
                    code: CERTOPS_API_TOKEN_NOT_FOUND
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/jobs/{jobId}/events:
    post:
      tags: [CertOps Agent]
      summary: Report an event for one CertOps job
      operationId: createCertOpsJobEvent
      description: Machine-token route for reporting lifecycle events scoped to the path jobId. Requires certops:events:write. Workspace identity is derived from the authenticated CertOps API token; body workspaceId cannot override it. The route uses the same private-key rejection, generic-secret redaction, idempotency, audit, and persistence path as POST /api/v1/certops/executor/events.
      security:
        - certOpsTokenAuth: []
      parameters:
        - in: path
          name: jobId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsPerJobExecutorEventRequest"
      responses:
        "202":
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsExecutorEventAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: CertOps is disabled or the certificate job was not found in the authenticated workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Executor event idempotency key conflicts with a different sanitized payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Executor event idempotency key conflicts with a different payload
                code: CERTOPS_EXECUTOR_EVENT_CONFLICT
        "413":
          description: The machine request body exceeded the dedicated 4 MiB pre-auth boundary (`CERTOPS_EXECUTOR_EVENT_BODY_TOO_LARGE`), or normalized evidence output exceeded 65,536 UTF-8 bytes (`CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE`). The payload is not echoed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps evidence output is too large
                code: CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "503":
          description: The security audit sink was unavailable while recording a private-key-material rejection. The endpoint fails closed; the payload is neither persisted nor echoed and no executor-event side effects occur.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps security audit is temporarily unavailable
                code: CERTOPS_SECURITY_AUDIT_UNAVAILABLE
        "429":
          description: CertOps machine-token rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps machine-token rate limit exceeded
                code: CERTOPS_MACHINE_RATE_LIMITED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/jobs/{jobId}/evidence:
    post:
      tags: [CertOps Agent]
      summary: Attach evidence to one CertOps job
      operationId: createCertOpsJobEvidence
      description: Machine-token route for attaching sanitized evidence to the path jobId. Requires certops:evidence:write. Workspace identity is derived from the authenticated CertOps API token; body workspaceId cannot override it. Private key material is rejected with 422, generic secrets are redacted, and optional output is limited to 65,536 UTF-8 bytes and stored only as redacted output.
      security:
        - certOpsTokenAuth: []
      parameters:
        - in: path
          name: jobId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsPerJobEvidenceRequest"
      responses:
        "202":
          description: Evidence accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsExecutorEventAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: CertOps is disabled or the certificate job was not found in the authenticated workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Executor event idempotency key conflicts with a different sanitized payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Executor event idempotency key conflicts with a different payload
                code: CERTOPS_EXECUTOR_EVENT_CONFLICT
        "413":
          description: The machine request body exceeded the dedicated 4 MiB pre-auth boundary (`CERTOPS_EXECUTOR_EVENT_BODY_TOO_LARGE`), or normalized evidence output exceeded 65,536 UTF-8 bytes (`CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE`). The payload is not echoed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps evidence output is too large
                code: CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "503":
          description: The security audit sink was unavailable while recording a private-key-material rejection. The endpoint fails closed; the payload is neither persisted nor echoed and no executor-event side effects occur.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps security audit is temporarily unavailable
                code: CERTOPS_SECURITY_AUDIT_UNAVAILABLE
        "429":
          description: CertOps machine-token rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps machine-token rate limit exceeded
                code: CERTOPS_MACHINE_RATE_LIMITED
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/executor/events:
    post:
      tags: [CertOps Agent]
      summary: Report executor events
      operationId: createCertOpsExecutorEvent
      description: >-
        M2 aggregate executor ingestion endpoint for normalized job and public
        evidence events. Lifecycle events require `certops:events:write`;
        evidence-bearing events additionally require `certops:evidence:write`.
        The route is hidden with 404 when `certops.enabled` is disabled.
        `eventId` is idempotent within a workspace and job: exact retries return
        202 without repeating logs, evidence, audit rows, or status side effects,
        while a different sanitized envelope using the same key returns 409. Late
        lifecycle events are recorded in the job log but cannot regress or reopen
        a terminal job. Payloads containing private key material are rejected with
        422. Generic secret material in metadata and bounded evidence output is
        redacted to `[REDACTED]` before persistence. Per-job event/evidence routes
        are narrow machine-token aliases that reuse the same hardened ingestion behavior.
      security:
        - certOpsTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertOpsExecutorEventRequest"
      responses:
        "202":
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertOpsExecutorEventAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Private key material is not accepted in CertOps requests
                code: PRIVATE_KEY_MATERIAL_REJECTED
        "503":
          description: The security audit sink was unavailable while recording a private-key-material rejection. The endpoint fails closed; the original payload is neither persisted nor echoed and no executor-event side effects occur.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps security audit is temporarily unavailable
                code: CERTOPS_SECURITY_AUDIT_UNAVAILABLE
        "409":
          description: Executor event idempotency key conflicts with a different sanitized payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Executor event idempotency key conflicts with a different payload
                code: CERTOPS_EXECUTOR_EVENT_CONFLICT
        "413":
          description: The executor event body exceeded the dedicated 4 MiB pre-auth boundary (`CERTOPS_EXECUTOR_EVENT_BODY_TOO_LARGE`), or normalized evidence output exceeded 65,536 UTF-8 bytes (`CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE`). The payload is not echoed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: CertOps evidence output is too large
                code: CERTOPS_EVIDENCE_OUTPUT_TOO_LARGE
        "429":
          description: The exact-path executor pre-auth or authenticated machine-token limiter rejected the request. The response never includes bearer-token material or request content.
          headers:
            Retry-After:
              description: Seconds until the caller may retry.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - type: object
                    required: [retryAfterSeconds]
                    properties:
                      retryAfterSeconds:
                        type: integer
                        minimum: 1
              example:
                error: CertOps machine-token rate limit exceeded
                code: CERTOPS_MACHINE_RATE_LIMITED
                retryAfterSeconds: 60
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/agent/register:
    post:
      tags: [CertOps Agent]
      summary: Register an agent
      operationId: createCertOpsAgentRegistration
      description: Outbound-only agent enrollment using a single-use bootstrap token; returns a per-agent credential shown once. Frozen namespace skeleton; body schema lands in M4.
      security:
        - agentBootstrapTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "201":
          description: Agent registered
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/agent/heartbeat:
    post:
      tags: [CertOps Agent]
      summary: Agent heartbeat
      operationId: createCertOpsAgentHeartbeat
      description: Agent heartbeat including reported clock offset (clock-drift detection). Per-agent credential authenticated. Frozen skeleton; body schema lands in M4.
      security:
        - agentCredentialAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Heartbeat accepted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/agent/jobs/claim:
    post:
      tags: [CertOps Agent]
      summary: Claim pending jobs
      operationId: createCertOpsAgentJobClaim
      description: Agent polls for and claims signed jobs. Per-agent credential authenticated. Frozen skeleton; returns signed job payloads (job-payload.schema.json) in M4.
      security:
        - agentCredentialAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Claimed jobs
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/certops/agent/jobs/results:
    post:
      tags: [CertOps Agent]
      summary: Report job results and evidence
      operationId: createCertOpsAgentJobResult
      description: Agent reports job results and evidence metadata. Per-agent credential authenticated. Payloads containing private key material are rejected (422). Frozen skeleton; body schema lands in M4.
      security:
        - agentCredentialAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "202":
          description: Result accepted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          description: Request rejected because it contained private key material
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "500":
          $ref: "#/components/responses/InternalError"
components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: sessionId
      description: Session cookie authentication used by authenticated API routes.
    certOpsTokenAuth:
      type: http
      scheme: bearer
      description: Scoped CertOps API token (bearer) used by M2 machine routes. Allowed M2 scopes are certops:read, certops:events:write, certops:jobs:read, and certops:evidence:write. certops:read implies certops:jobs:read only, never a write scope. Bearer-only and non-cookie; CSRF-exempt only for the exact planned POST machine routes. Empty required-scope configuration is invalid.
    agentBootstrapTokenAuth:
      type: http
      scheme: bearer
      description: Single-use, hashed, expiring bootstrap enrollment token used only by POST /api/v1/certops/agent/register. Exchanged for a per-agent credential at registration.
    agentCredentialAuth:
      type: http
      scheme: bearer
      description: Per-agent credential (bearer) issued at registration, used for agent heartbeat, job claim, and result/evidence calls. CSRF-exempt. Not valid for registration.
  parameters:
    workspaceIdParam:
      in: path
      name: id
      required: true
      schema:
        type: string
        format: uuid
    tokenIdParam:
      in: path
      name: id
      required: true
      schema:
        type: integer
    userIdParam:
      in: path
      name: userId
      required: true
      schema:
        type: integer
    tokenParam:
      in: path
      name: token
      required: true
      schema:
        type: string
    limitParam:
      in: query
      name: limit
      schema:
        type: integer
        minimum: 1
    offsetParam:
      in: query
      name: offset
      schema:
        type: integer
        minimum: 0
    certOpsReadLimitParam:
      in: query
      name: limit
      description: >-
        Number of items returned. Defaults to 50 when omitted or nonnumeric,
        is limited to 100, and values below 1 are normalized to 1.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    certOpsReadOffsetParam:
      in: query
      name: offset
      description: >-
        Number of items skipped before the page begins. Defaults to 0 when
        omitted or nonnumeric; negative values are normalized to 0.
      schema:
        type: integer
        minimum: 0
        default: 0
    auditSinceParam:
      in: query
      name: since
      description: >-
        Lower bound for occurred_at filtering. Accepts ISO 8601 UTC (recommended),
        YYYY-MM-DD (start of day), or a datetime without timezone
        (YYYY-MM-DDTHH:mm, optional seconds).
      schema:
        type: string
        example: "2026-06-04"
    auditUntilParam:
      in: query
      name: until
      description: >-
        Upper bound for occurred_at filtering. Accepts ISO 8601 UTC (recommended),
        YYYY-MM-DD (end of day), or a datetime without timezone
        (YYYY-MM-DDTHH:mm, optional seconds).
      schema:
        type: string
        example: "2026-06-04"
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    Pagination:
      type: object
      properties:
        limit:
          type: integer
        offset:
          type: integer
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        details:
          oneOf:
            - type: array
              items:
                type: string
            - type: object
              additionalProperties: true
            - type: string
      example:
        error: Invalid request payload
        code: VALIDATION_ERROR
        details:
          - email is required
    CertOpsManagedCertificate:
      type: object
      additionalProperties: false
      description: Public-only CertOps managed certificate inventory record. This schema intentionally excludes private-key custody fields; private key material is rejected at the API boundary.
      required:
        - schemaVersion
        - id
        - workspaceId
        - tokenId
        - profileId
        - status
        - source
        - sourceRef
        - commonName
        - subjectAltNames
        - issuer
        - serialNumber
        - fingerprintSha256
        - spkiFingerprintSha256
        - certificatePem
        - publicKeyAlgorithm
        - publicKeySize
        - signatureAlgorithm
        - notBefore
        - notAfter
        - keyMode
        - keyReference
        - createdAt
        - updatedAt
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        tokenId:
          type: integer
          nullable: true
        profileId:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
          enum:
            - discovered
            - active
            - renewing
            - expiring
            - expired
            - revoked
            - decommissioned
        source:
          type: string
          nullable: true
          enum:
            - manual
            - api
            - import
            - domain_checker
            - endpoint_monitor
            - integration
            - auto_sync
            - null
        sourceRef:
          type: string
          nullable: true
          description: Opaque non-secret source reference.
        commonName:
          type: string
          nullable: true
        subjectAltNames:
          type: array
          items:
            type: string
        issuer:
          type: string
          nullable: true
        serialNumber:
          type: string
          nullable: true
        fingerprintSha256:
          type: string
          nullable: true
          pattern: "^[a-f0-9]{64}$"
          description: Lowercase SHA-256 fingerprint of public certificate material.
        spkiFingerprintSha256:
          type: string
          nullable: true
          pattern: "^[a-f0-9]{64}$"
          description: Lowercase SHA-256 fingerprint of the public SubjectPublicKeyInfo.
        certificatePem:
          type: string
          nullable: true
          description: Public certificate PEM only. Private-key PEM, PFX/PKCS#12, and key-bearing material are rejected.
        publicKeyAlgorithm:
          type: string
          nullable: true
        publicKeySize:
          type: integer
          minimum: 1
          nullable: true
        signatureAlgorithm:
          type: string
          nullable: true
        notBefore:
          type: string
          format: date-time
          nullable: true
        notAfter:
          type: string
          format: date-time
          nullable: true
        keyMode:
          type: string
          nullable: true
          enum:
            - agent-local
            - proxy-agent-local
            - cert-manager-managed
            - appliance-managed
            - hsm-managed
            - vault-managed
            - os-store-managed
            - external-unknown
            - null
          description: Non-secret mode describing where the private key lives outside the control plane.
        keyReference:
          type: string
          nullable: true
          maxLength: 256
          description: Opaque non-secret external reference. Inline secrets, private-key material, PEM markers, and control characters are rejected.
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
    CertOpsManagedCertificateListResponse:
      type: object
      additionalProperties: false
      required: [items, pagination]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsManagedCertificate"
        pagination:
          type: object
          additionalProperties: false
          required: [limit, offset]
          properties:
            limit:
              type: integer
              minimum: 1
            offset:
              type: integer
              minimum: 0
    CertOpsManagedCertificateDetailResponse:
      type: object
      additionalProperties: false
      required: [certificate]
      properties:
        certificate:
          $ref: "#/components/schemas/CertOpsManagedCertificate"
    CertOpsCertificateRetireRequest:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          enum:
            - revoked
            - decommissioned
          description: Soft-retire lifecycle status to apply to the managed certificate and linked token.
        reason:
          type: string
          nullable: true
          maxLength: 512
          description: Optional non-secret retirement reason. Private-key material, PEM markers, and secret-looking assignments are rejected.
    CertOpsCertificateWriteResponse:
      type: object
      additionalProperties: false
      required: [items, count]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsManagedCertificate"
        count:
          type: integer
          minimum: 0
    CertOpsCertificateWriteRequest:
      type: object
      additionalProperties: true
      description: Recognized fields for public certificate import/register operations. Unknown fields are ignored by the current runtime, but the entire request body is scanned and rejected if any field contains private key material.
      anyOf:
        - required: [certificatePem]
        - required: [pem]
        - required: [certificate]
        - required: [chainPem]
        - required: [certificates]
      properties:
        certificatePem:
          type: string
          description: Public certificate PEM or PEM chain. Private key material is rejected with PRIVATE_KEY_MATERIAL_REJECTED.
        pem:
          type: string
          description: Public certificate PEM or PEM chain.
        certificate:
          type: string
          description: Public certificate PEM or PEM chain.
        chainPem:
          type: string
          description: Public certificate PEM chain.
        certificates:
          type: array
          minItems: 1
          items:
            type: string
          description: Public certificate PEM blocks joined by the runtime into a chain.
        sourceRef:
          type: string
          nullable: true
          description: Opaque non-secret source reference.
        name:
          type: string
          nullable: true
        keyMode:
          type: string
          nullable: true
          enum:
            - agent-local
            - proxy-agent-local
            - cert-manager-managed
            - appliance-managed
            - hsm-managed
            - vault-managed
            - os-store-managed
            - external-unknown
            - null
          description: Non-secret mode describing where the private key lives outside the control plane.
        keyReference:
          type: string
          nullable: true
          maxLength: 256
          description: Opaque non-secret external reference. Invalid references return CERTOPS_KEY_REFERENCE_INVALID; private-key material returns PRIVATE_KEY_MATERIAL_REJECTED.
    CertOpsMetadataEntry:
      type: object
      additionalProperties: false
      description: Public metadata only. Names colliding after case/separator normalization with server-owned executor, evidence, lifecycle, or redaction fields are rejected.
      x-certops-reservedMetadataNames: [executorEventId, executorEventType, executorStatus, occurredAt, certificateId, executorId, evidenceId, certificateInstanceId, targetId, source, status, fingerprintSha256, summary, artifactRefs, redactionApplied, redactionCount, redactedFields, redactedSecretCategories, jobStatusTransitionApplied, jobStatusTransitionIgnored, jobStatusTransitionIgnoredReason]
      required: [name, value]
      properties:
        name:
          type: string
          maxLength: 64
          description: Public metadata name. Private-key-bearing names are prohibited. Generic-secret-bearing names are accepted only for server-side redaction and persist under server-owned redacted metadata keys. Names colliding with x-certops-reservedMetadataNames are rejected.
          pattern: "^(?!.*(?:[Pp][Rr][Ii][Vv][Aa][Tt][Ee][-_]?[Kk][Ee][Yy]|[Ee][Nn][Cc][Rr][Yy][Pp][Tt][Ee][Dd][-_]?[Pp][Rr][Ii][Vv][Aa][Tt][Ee][-_]?[Kk][Ee][Yy]|[Kk][Ee][Yy][-_]?[Mm][Aa][Tt][Ee][Rr][Ii][Aa][Ll]|[Pp][Ff][Xx][-_]?[Bb][Ll][Oo][Bb]|[Jj][Kk][Ss][-_]?[Bb][Ll][Oo][Bb]|[Tt][Ll][Ss][-_]?[Kk][Ee][Yy]|[Cc][Aa][-_]?[Pp][Rr][Ii][Vv][Aa][Tt][Ee][-_]?[Kk][Ee][Yy]|[Kk][Ee][Yy][Ss][Tt][Oo][Rr][Ee][-_]?[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee][-_]?[Kk][Ee][Yy][-_]?[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Kk][Ee][Yy][-_]?[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Kk][Ee][Yy][-_]?[Pp][Ee][Mm]))[A-Za-z0-9_.:-]{1,64}$"
        value:
          oneOf:
            - type: string
              maxLength: 512
            - type: number
            - type: integer
            - type: boolean
    CertOpsJob:
      type: object
      additionalProperties: false
      description: Public CertOps job summary for dashboard and timeline reads. Payload and result metadata are omitted from list responses.
      required:
        - id
        - workspaceId
        - operation
        - status
        - source
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        operation:
          type: string
          enum: [renew, deploy, reload, revoke, noop]
        status:
          type: string
          enum:
            - pending_approval
            - approved
            - rejected
            - pending
            - claimed
            - running
            - succeeded
            - failed
            - blocked
            - cancelled
        source:
          type: string
          enum: [api, executor, system, automation, domain-monitor, endpoint-monitor, control-plane, external]
        subjectType:
          type: string
          nullable: true
          enum: [managed_certificate, certificate_instance, certificate_target, token, domain, endpoint, external]
        subjectId:
          type: string
          maxLength: 128
          nullable: true
        requestedByUserId:
          type: integer
          nullable: true
        requestedByApiTokenId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        queuedAt:
          type: string
          format: date-time
          nullable: true
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        cancelledAt:
          type: string
          format: date-time
          nullable: true
    CertOpsJobListResponse:
      type: object
      additionalProperties: false
      required: [items, pagination]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsJob"
        pagination:
          $ref: "#/components/schemas/Pagination"
    CertOpsJobCreateRequest:
      type: object
      additionalProperties: false
      required: [operation]
      description: >-
        Manual job creation request. source is always forced to "api" by the
        server and is not accepted from the request body.
      properties:
        operation:
          type: string
          enum: [renew, deploy, reload, revoke, noop]
        subjectType:
          type: string
          enum: [managed_certificate, certificate_instance, certificate_target, token, domain, endpoint, external]
        subjectId:
          type: string
          maxLength: 128
        payload:
          $ref: "#/components/schemas/CertOpsPublicObject"
        idempotencyKey:
          type: string
          maxLength: 128
    CertOpsPublicObject:
      type: object
      additionalProperties: true
      description: Sanitized public metadata object. Runtime rejects private-key material, credentials, token secrets, raw secret dumps, and custody-shaped field names before persistence.
    CertOpsJobDetail:
      type: object
      additionalProperties: false
      required: [id, workspaceId, operation, status, source, createdAt, updatedAt]
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        operation:
          type: string
          enum: [renew, deploy, reload, revoke, noop]
        status:
          type: string
          enum: [pending_approval, approved, rejected, pending, claimed, running, succeeded, failed, blocked, cancelled]
        source:
          type: string
          enum: [api, executor, system, automation, domain-monitor, endpoint-monitor, control-plane, external]
        subjectType:
          type: string
          nullable: true
          enum: [managed_certificate, certificate_instance, certificate_target, token, domain, endpoint, external]
        subjectId:
          type: string
          maxLength: 128
          nullable: true
        requestedByUserId:
          type: integer
          nullable: true
        requestedByApiTokenId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        queuedAt:
          type: string
          format: date-time
          nullable: true
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        cancelledAt:
          type: string
          format: date-time
          nullable: true
        payload:
          $ref: "#/components/schemas/CertOpsPublicObject"
        resultMetadata:
          $ref: "#/components/schemas/CertOpsPublicObject"
        errorCode:
          type: string
          maxLength: 128
          nullable: true
        errorMessage:
          type: string
          maxLength: 1024
          nullable: true
    CertOpsJobDetailResponse:
      type: object
      additionalProperties: false
      required: [job]
      properties:
        job:
          $ref: "#/components/schemas/CertOpsJobDetail"
    CertOpsJobLogEntry:
      type: object
      additionalProperties: false
      required: [id, workspaceId, jobId, eventType, createdAt]
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        jobId:
          type: string
          format: uuid
        eventType:
          type: string
          enum: [job.created, job.accepted, job.started, job.progress, job.completed, job.failed, job.rejected, job.cancelled, job.status_updated, evidence.attached]
        status:
          type: string
          nullable: true
          enum: [pending_approval, approved, rejected, pending, claimed, running, succeeded, failed, blocked, cancelled]
        message:
          type: string
          maxLength: 1024
          nullable: true
        metadata:
          $ref: "#/components/schemas/CertOpsPublicObject"
        createdByUserId:
          type: integer
          nullable: true
        createdByApiTokenId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
    CertOpsJobLogListResponse:
      type: object
      additionalProperties: false
      required: [items, pagination]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsJobLogEntry"
        pagination:
          $ref: "#/components/schemas/Pagination"
    CertOpsEvidenceItem:
      type: object
      additionalProperties: false
      required: [id, workspaceId, evidenceType, metadata, createdAt]
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        jobId:
          type: string
          format: uuid
          nullable: true
        evidenceType:
          type: string
          enum: [certificate.observed, deployment.checked, deployment.updated, validation.passed, validation.failed, policy.checked]
        subjectType:
          type: string
          nullable: true
          enum: [managed_certificate, certificate_instance, certificate_target, token, domain, endpoint, external]
        subjectId:
          type: string
          maxLength: 128
          nullable: true
        metadata:
          $ref: "#/components/schemas/CertOpsPublicObject"
        observedAt:
          type: string
          format: date-time
          nullable: true
        createdByUserId:
          type: integer
          nullable: true
        createdByApiTokenId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
    CertOpsEvidenceListResponse:
      type: object
      additionalProperties: false
      required: [items, pagination]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsEvidenceItem"
        pagination:
          $ref: "#/components/schemas/Pagination"
    CertOpsApiTokenScope:
      type: string
      enum:
        - certops:read
        - certops:events:write
        - certops:jobs:read
        - certops:evidence:write
      description: Canonical M2 CertOps API-token scope. Job-claim and signed-agent scopes are deferred to M4.
    CertOpsApiToken:
      type: object
      additionalProperties: false
      description: Public CertOps API-token metadata. Plaintext token values, token hashes, Authorization headers, and credential material are never returned by metadata/list/revoke responses.
      required:
        - id
        - workspaceId
        - name
        - tokenPrefix
        - scopes
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 128
        tokenPrefix:
          type: string
          minLength: 1
          maxLength: 64
          pattern: "^ttx_[a-f0-9]{16}$"
          description: Public lookup prefix only; not sufficient to authenticate.
        scopes:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          items:
            $ref: "#/components/schemas/CertOpsApiTokenScope"
        status:
          type: string
          enum: [active, revoked, expired]
        expiresAt:
          type: string
          format: date-time
          nullable: true
        revokedAt:
          type: string
          format: date-time
          nullable: true
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        revokedByUserId:
          type: integer
          nullable: true
        createdByUserId:
          type: integer
          nullable: true
    CertOpsApiTokenListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CertOpsApiToken"
    CertOpsApiTokenCreateRequest:
      type: object
      additionalProperties: false
      required: [name, scopes]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
          description: Public label. Must not contain a raw CertOps token, bearer credential, token hash, private-key material, or generic credential material.
        scopes:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          items:
            $ref: "#/components/schemas/CertOpsApiTokenScope"
        expiresAt:
          type: string
          format: date-time
          nullable: true
    CertOpsApiTokenCreateResponse:
      type: object
      additionalProperties: false
      description: Token creation response. plaintextToken is returned once at creation only; it is never returned by list/get/revoke responses and is never stored in recoverable form.
      required: [token, plaintextToken]
      properties:
        token:
          $ref: "#/components/schemas/CertOpsApiToken"
        plaintextToken:
          type: string
          minLength: 85
          maxLength: 85
          pattern: "^ttx_[a-f0-9]{16}_[a-f0-9]{64}$"
          description: One-time plaintext token value shown only by the create response.
    CertOpsApiTokenRevokeResponse:
      type: object
      additionalProperties: false
      required: [token]
      properties:
        token:
          $ref: "#/components/schemas/CertOpsApiToken"
    CertOpsEvidenceMetadata:
      type: object
      additionalProperties: false
      description: Strict standalone normalized/persisted public evidence metadata. M2-A10 separately stores bounded executor output after private-key rejection and generic-secret redaction; raw output and key-bearing artifacts are excluded here.
      required:
        - schemaVersion
        - evidenceId
        - jobId
        - workspaceId
        - eventType
        - source
        - observedAt
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        evidenceId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        jobId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        workspaceId:
          type: string
          format: uuid
        certificateId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        certificateInstanceId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        targetId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        eventType:
          type: string
          enum:
            - certificate.observed
            - deployment.checked
            - deployment.updated
            - validation.passed
            - validation.failed
            - policy.checked
        source:
          type: string
          enum: [executor, domain-monitor, endpoint-monitor, control-plane, external]
        status:
          type: string
          enum: [accepted, redacted, rejected, failed]
          nullable: true
        observedAt:
          type: string
          format: date-time
          pattern: "^(?:200[0-9]|20[1-9][0-9]|2100)-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))$"
          description: The supplied RFC3339 timestamp must use a year from 2000 through 2100. Runtime normalizes accepted values to UTC milliseconds; offsets may move the normalized instant outside that lexical-year range.
        fingerprintSha256:
          type: string
          pattern: "^[a-f0-9]{64}$"
          nullable: true
        summary:
          type: string
          maxLength: 1024
          nullable: true
        metadata:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/CertOpsMetadataEntry"
        artifactRefs:
          type: array
          maxItems: 16
          items:
            type: object
            additionalProperties: false
            required: [type, reference]
            properties:
              type:
                type: string
                enum: [log, report, certificate, deployment, external]
              reference:
                type: string
                minLength: 1
                maxLength: 512
              sha256:
                type: string
                pattern: "^[a-f0-9]{64}$"
                nullable: true
        output:
          type: string
          maxLength: 65536
          x-maxBytes: 65536
          nullable: true
          description: Optional executor output, limited to 65,536 UTF-8 bytes. Runtime rejects private key material, redacts generic secrets to [REDACTED], stores only redacted output separately from normalized metadata, and does not return this field from evidence list responses.
        redactionApplied:
          type: boolean
          nullable: true
    CertOpsEmbeddedExecutorEvidenceItem:
      type: object
      additionalProperties: false
      description: Relaxed embedded evidence item for the M2 executor event envelope. Only eventType is required at the embedded boundary; later runtime may derive or soft-fill jobId, workspaceId, source, and observedAt from the envelope/path. Standalone normalized evidence remains strict in CertOpsEvidenceMetadata. Raw executor output is deferred to M2-A10.
      required: [eventType]
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        evidenceId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        jobId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        workspaceId:
          type: string
          format: uuid
        certificateId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        certificateInstanceId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        targetId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        eventType:
          type: string
          enum:
            - certificate.observed
            - deployment.checked
            - deployment.updated
            - validation.passed
            - validation.failed
            - policy.checked
        source:
          type: string
          enum: [executor, domain-monitor, endpoint-monitor, control-plane, external]
          nullable: true
        status:
          type: string
          enum: [accepted, redacted, rejected, failed]
          nullable: true
        observedAt:
          type: string
          format: date-time
          pattern: "^(?:200[0-9]|20[1-9][0-9]|2100)-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))$"
          description: The supplied RFC3339 timestamp must use a year from 2000 through 2100. Runtime normalizes accepted values to UTC milliseconds; offsets may move the normalized instant outside that lexical-year range.
          nullable: true
        fingerprintSha256:
          type: string
          pattern: "^[a-f0-9]{64}$"
          nullable: true
        summary:
          type: string
          maxLength: 1024
          nullable: true
        metadata:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/CertOpsMetadataEntry"
        artifactRefs:
          type: array
          maxItems: 16
          items:
            type: object
            additionalProperties: false
            required: [type, reference]
            properties:
              type:
                type: string
                enum: [log, report, certificate, deployment, external]
              reference:
                type: string
                minLength: 1
                maxLength: 512
              sha256:
                type: string
                pattern: "^[a-f0-9]{64}$"
                nullable: true
        output:
          type: string
          maxLength: 65536
          x-maxBytes: 65536
          nullable: true
          description: Optional executor output, limited to 65,536 UTF-8 bytes. Runtime rejects private key material, redacts generic secrets to [REDACTED], stores only redacted output separately from normalized metadata, and does not return this field from evidence list responses.
        redactionApplied:
          type: boolean
          nullable: true
    CertOpsExecutorEventRequest:
      type: object
      additionalProperties: false
      description: M2 external executor event envelope reconciled with TOKENTIMER_CERTOPS_PLAN v1.7. eventId is an idempotency key within workspaceId and jobId. Private key material is rejected; generic secrets in log and evidence metadata are redacted before persistence. Older operational lifecycle names are represented as normalized job/evidence events; richer agent-executed protocol and job signing are deferred to M4.
      required:
        - schemaVersion
        - eventId
        - jobId
        - workspaceId
        - status
        - eventType
        - occurredAt
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        eventId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          description: Executor-supplied idempotency key scoped to workspaceId and jobId.
        jobId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        workspaceId:
          type: string
          format: uuid
        certificateId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        executorId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        status:
          type: string
          enum:
            - accepted
            - claimed
            - running
            - succeeded
            - failed
            - rejected
            - blocked
            - cancelled
        eventType:
          type: string
          enum:
            - job.accepted
            - job.started
            - job.progress
            - job.completed
            - job.failed
            - job.rejected
            - evidence.attached
        occurredAt:
          type: string
          format: date-time
          pattern: "^(?:200[0-9]|20[1-9][0-9]|2100)-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))$"
          description: The supplied RFC3339 timestamp must use a year from 2000 through 2100. Runtime normalizes accepted values to UTC milliseconds; offsets may move the normalized instant outside that lexical-year range.
        message:
          type: string
          maxLength: 1024
          nullable: true
        evidence:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/CertOpsEmbeddedExecutorEvidenceItem"
        metadata:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/CertOpsMetadataEntry"
      oneOf:
        - properties:
            eventType:
              enum: [job.accepted, job.started, job.progress, job.completed, job.failed, job.rejected]
        - required: [evidence]
          properties:
            eventType:
              enum: [evidence.attached]
            evidence:
              minItems: 1
    CertOpsPerJobExecutorEventRequest:
      type: object
      additionalProperties: false
      description: Per-job machine event request. The path jobId is authoritative. Optional body jobId must match the path, and optional body workspaceId must match the authenticated API token workspace. Private key material is rejected; generic secrets in metadata are redacted.
      required:
        - schemaVersion
        - eventId
        - status
        - eventType
        - occurredAt
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        eventId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        jobId:
          type: string
          format: uuid
          nullable: true
        workspaceId:
          type: string
          format: uuid
          nullable: true
        certificateId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        executorId:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          nullable: true
        status:
          type: string
          enum: [accepted, claimed, running, succeeded, failed, rejected, blocked, cancelled]
        eventType:
          type: string
          enum:
            - job.accepted
            - job.started
            - job.progress
            - job.completed
            - job.failed
            - job.rejected
            - evidence.attached
        occurredAt:
          type: string
          format: date-time
        message:
          type: string
          maxLength: 1024
          nullable: true
        evidence:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/CertOpsEmbeddedExecutorEvidenceItem"
        metadata:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/CertOpsMetadataEntry"
      oneOf:
        - properties:
            eventType:
              enum: [job.accepted, job.started, job.progress, job.completed, job.failed, job.rejected]
        - required: [evidence]
          properties:
            eventType:
              enum: [evidence.attached]
            evidence:
              minItems: 1
    CertOpsPerJobEvidenceRequest:
      type: object
      additionalProperties: false
      description: Per-job machine evidence request. The path jobId is authoritative and the server normalizes eventType to evidence.attached. Requires certops:evidence:write. Optional output is private-key rejected, generic-secret redacted, and limited to 65,536 UTF-8 bytes before persistence.
      required:
        - schemaVersion
        - eventId
        - occurredAt
        - evidence
      properties:
        schemaVersion:
          type: integer
          enum: [1]
        eventId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
        jobId:
          type: string
          format: uuid
          nullable: true
        workspaceId:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
          enum: [accepted]
          nullable: true
        eventType:
          type: string
          enum: [evidence.attached]
          nullable: true
        occurredAt:
          type: string
          format: date-time
        message:
          type: string
          maxLength: 1024
          nullable: true
        evidence:
          type: array
          minItems: 1
          maxItems: 16
          items:
            $ref: "#/components/schemas/CertOpsEmbeddedExecutorEvidenceItem"
        metadata:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/CertOpsMetadataEntry"
    CertOpsExecutorEventAcceptedResponse:
      type: object
      additionalProperties: false
      required: [ok, eventId, jobId, status, redactionApplied, redactionCount]
      properties:
        ok:
          type: boolean
          enum: [true]
        eventId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_.:-]+$"
          description: Server-side job log event identifier for the accepted executor event.
        logId:
          type: string
          format: uuid
          nullable: true
          description: Server-side job log event identifier; matches eventId for M2 runtime responses.
        jobId:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending_approval, approved, rejected, pending, claimed, running, succeeded, failed, blocked, cancelled]
        redactionApplied:
          type: boolean
          description: True when generic-secret redaction affected accepted event metadata or evidence output.
        redactionCount:
          type: integer
          minimum: 0
          description: Count of generic-secret redactions represented by this response.
        evidenceId:
          type: string
          format: uuid
          nullable: true
        evidenceIds:
          type: array
          maxItems: 16
          items:
            type: string
            format: uuid
        executorEventRecordId:
          type: string
          format: uuid
          nullable: true
          description: Safe idempotency record identifier for this accepted executor event.
        duplicate:
          type: boolean
          default: false
          description: True when this response came from an idempotent retry and no new side effects were created.
        idempotent:
          type: boolean
          description: True when the request reused an existing accepted event response.
    SessionUser:
      type: object
      required: [id, email]
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        displayName:
          type: string
        plan:
          type: string
        authMethod:
          type: string
        loginMethod:
          type: string
        emailVerified:
          type: boolean
        needsVerification:
          type: boolean
        twoFactorEnabled:
          type: boolean
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: admin@localhost.local
        password:
          type: string
          example: AdminPassword123!
    LoginSuccessResponse:
      type: object
      required: [message, user]
      properties:
        message:
          type: string
        user:
          type: object
          properties:
            id:
              type: integer
            email:
              type: string
            displayName:
              type: string
    LoginNeeds2FAResponse:
      type: object
      required: [requires2FA]
      properties:
        requires2FA:
          type: boolean
        authMethod:
          type: string
    Workspace:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        plan:
          type: string
        role:
          type: string
          enum: [admin, workspace_manager, viewer]
        created_by:
          type: integer
        is_personal:
          type: boolean
        is_personal_default:
          type: boolean
    WorkspaceCreateResponse:
      type: object
      required: [id, name, plan, role]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        plan:
          type: string
        role:
          type: string
      example:
        id: "8d13ecfe-9436-4f09-b8ba-0248c5b4e8ef"
        name: "Production Workspace"
        plan: "oss"
        role: "admin"
    WorkspaceMember:
      type: object
      properties:
        user_id:
          type: integer
        role:
          type: string
        display_name:
          type: string
        email:
          type: string
          format: email
    TokenCreateRequest:
      type: object
      required: [workspace_id, name, type]
      properties:
        workspace_id:
          type: string
          format: uuid
          example: "8d13ecfe-9436-4f09-b8ba-0248c5b4e8ef"
        name:
          type: string
          example: "Stripe Production API Key"
        type:
          type: string
          example: "api_key"
        category:
          type: string
          example: "payment"
        expiresAt:
          type: string
          format: date
          example: "2027-12-31"
        confirm_duplicate:
          type: boolean
          example: false
        contact_group_id:
          type: string
          example: "d7c5f1be-15f4-4b5b-9b49-f80f0d48d085"
        description:
          type: string
          example: "Rotated quarterly by platform team"
        notes:
          type: string
          example: "Owner: Platform - escalate before expiration"
    Token:
      type: object
      required: [id, name, type]
      properties:
        id:
          type: integer
        workspace_id:
          type: string
          format: uuid
        name:
          type: string
        type:
          type: string
        category:
          type: string
        expiration:
          type: string
          format: date
        expiresAt:
          type: string
          format: date
        location:
          type: string
        issuer:
          type: string
        serial_number:
          type: string
        subject:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      example:
        id: 142
        workspace_id: "8d13ecfe-9436-4f09-b8ba-0248c5b4e8ef"
        name: "Stripe Production API Key"
        type: "api_key"
        category: "payment"
        expiresAt: "2027-12-31"
        created_at: "2026-03-13T18:00:00.000Z"
        updated_at: "2026-03-13T18:00:00.000Z"
    AuditEvent:
      type: object
      properties:
        id:
          type: integer
        occurred_at:
          type: string
          format: date-time
        actor_user_id:
          type: integer
        actor_display_name:
          type: string
        subject_user_id:
          type: integer
        action:
          type: string
        target_type:
          type: string
        target_id:
          oneOf:
            - type: integer
            - type: string
        channel:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        workspace_id:
          type: string
          format: uuid
          nullable: true
        workspace_name:
          type: string
    ProviderScanRequest:
      type: object
      additionalProperties: true
      properties:
        provider:
          type: string
        credentials:
          type: object
          additionalProperties: true
    VaultScanRequest:
      type: object
      required: [address, token]
      properties:
        address:
          type: string
          format: uri
        token:
          type: string
        include:
          type: object
          additionalProperties: true
        mounts:
          type: array
          items:
            type: string
    VaultMountsRequest:
      type: object
      required: [address, token]
      properties:
        address:
          type: string
          format: uri
        token:
          type: string
    IntegrationScanResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            additionalProperties: true
        quota:
          type: object
          additionalProperties: true
    IntegrationImportRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          items:
            type: object
            additionalProperties: true
        workspace_id:
          type: string
          format: uuid
        default_category:
          type: string
        default_type:
          type: string
    IntegrationImportResponse:
      type: object
      properties:
        created:
          type: integer
        updated:
          type: integer
        errors:
          type: array
          items:
            type: object
            additionalProperties: true
