openapi: 3.1.1
info:
  title: FermiHDI DMS Northbound API
  description: |
    Management API for the DMS WebUX and automated administrative tools.

    ## Security Model

    ### Northbound mTLS (Port 8443)
    The Northbound API requires **Mutual TLS**. The peer must present an X.509 client certificate
    signed by the DMS-CA. In production, Traefik acts as the mTLS client toward DMS-Core using a
    `traefik-client` certificate issued by the DMS-CA via `POST /v1/sign`.

    ### Edge TLS (Traefik → Browser)
    Traefik's **server certificate** (presented to browsers and admin tools) is provisioned by the
    **Administrator or SRE** and is completely independent of the DMS-CA. Typical options are:
    - Let's Encrypt (ACME) for public deployments
    - Enterprise CA certificate (e.g., internal PKI)
    - Self-signed certificate for lab environments

    The DMS-CA **never** issues certificates for Traefik's edge listener. This boundary is by design:
    the DMS-CA is a private mesh CA scoped to FermiHDI mTLS identities, not a public trust anchor.

    ### Setup Mode
    Before a valid license is installed, DMS-Core operates in Setup Mode. Only `/v1/license/apply`
    and `/v1/license/request` are accessible. All other routes return `503 Service Unavailable`.
    Setup Mode authentication uses a shared `FERMIHDI_INTERNAL_FE_TOKEN` Bearer token rather than mTLS.
  version: 1.2.0
servers:
  - url: https://{dms_host}:8443
    description: Direct Northbound mTLS access
    variables:
      dms_host:
        default: dms.fermihdi.local
  - url: https://{edge_host}
    description: Via administrator-provisioned Traefik edge proxy
    variables:
      edge_host:
        default: dms.example.com

paths:
  /v1/nodes:
    get:
      summary: List Cluster Nodes
      description: Returns all registered nodes with their latest telemetry.
      security:
        - mtls: []
      responses:
        '200':
          description: List of nodes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RegisteredNode'

  /v1/deployments:
    get:
      summary: List HD Instances
      description: Returns all logical HD Instances managed by this DMS.
      security:
        - mtls: []
      responses:
        '200':
          description: List of instances
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/HDInstance'
    post:
      summary: Register HD Instance
      description: Creates a new logical HD Instance.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HDInstance'
      responses:
        '201':
          description: Instance registered

  /v1/deployments/sc:
    post:
      summary: Add Storage Cluster to Instance
      description: Links a Storage Cluster (SC) to an HD Instance and triggers a routing update push to all Proxy and Ingest nodes.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [instance_id, sc_name]
              properties:
                instance_id: { type: string }
                sc_name: { type: string }
      responses:
        '201':
          description: Storage Cluster linked and routing pushed

  /v1/deployments/sc_status:
    post:
      summary: Toggle Storage Cluster Maintenance State
      description: Safely transitions a discrete Storage Cluster between Active and Maintenance states, dynamically propagating re-routes across Proxy and Target bounds.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [instance_id, sc_name, status]
              properties:
                instance_id: { type: string }
                sc_name: { type: string }
                status: { type: string, enum: [Active, Maintenance] }
      responses:
        '202':
          description: Status toggle accepted and successfully pushed downstream

  /v1/nodes/provision:
    post:
      summary: Provision Node Identity
      description: Pre-authorizes a Node ID and Type before a physical node registers.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node_id, node_type, instance_id]
              properties:
                node_id: { type: string }
                node_type: { type: string }
                instance_id: { type: string }
                generate_token: 
                  type: boolean
                  description: "If true, generates a 32-char hex bootstrap token for auto-approval."
                generate_credentials:
                  type: boolean
                  description: "If true, generates a full RSA private key and signed certificate for manual injection."
      responses:
        '201':
          description: Node provisioned and identity generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  node_id: { type: string }
                  token: { type: string, description: "Generated bootstrap token (if requested)" }
                  private_key: { type: string, description: "Generated RSA private key (if requested)" }
                  certificate: { type: string, description: "Generated signed certificate (if requested)" }
        '403':
          description: License capacity exceeded

  /v1/nodes/approve:
    post:
      summary: Approve Pending Node
      description: Manually signs the CSR for a node in 'Pending Approval' state.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node_id]
              properties:
                node_id: { type: string }
      responses:
        '200':
          description: Node approved and certificate signed
        '404':
          description: Node not found or not in pending state

  /v1/nodes/shutdown:
    post:
      summary: Graceful Shutdown
      description: Triggers a graceful shutdown cascade for a target Storage Node or Storage Cluster Controller.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node_id]
              properties:
                node_id: { type: string }
      responses:
        '202':
          description: Shutdown request accepted and emitted to bounds
        '404':
          description: Node not found

  /v1/nodes/revoke:
    post:
      summary: Revoke Node Access
      description: Invalidates a node's certificate and marks it as Inactive.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node_id]
              properties:
                node_id: { type: string }
      responses:
        '202':
          description: Revocation accepted

  /v1/nodes/config:
    post:
      summary: Update Node Configuration
      description: Pushes storage and alerting strategies to a registered node.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node_id, config]
              properties:
                node_id: { type: string }
                config: { $ref: '#/components/schemas/NodeConfig' }
      responses:
        '202':
          description: Configuration updated

  /v1/license/request:
    get:
      summary: Request CSR for Day-0 Initialization
      description: |
        Generates an ephemeral RSA private key for `dms-core`, returning the PEM-encoded CSR to the
        WebUX. Only available in Setup Mode. In Production Mode returns `503`.
      responses:
        '200':
          description: CSR PEM payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  csr: { type: string, description: "PEM-encoded PKCS#10 CSR" }
        '503':
          description: System is in Production Mode — CSR generation not available

  /v1/license:
    get:
      summary: Get Current License
      description: Returns the decrypted license details currently active in the DMS.
      security:
        - mtls: []
      responses:
        '200':
          description: Active license details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicenseFile'

  /v1/license/apply:
    post:
      summary: Upload and Apply a License Bundle
      description: |
        Accepts an encrypted FermiHDI license bundle. On success:
        1. Decrypts and validates the bundle signature against the FermiHDI Root CA.
        2. Installs the DMS-Core certificate from the bundle.
        3. Forwards the new ICA cert and key to the DMS-CA (`POST /v1/license/apply` on :6989).
        4. The DMS-CA validates the ICA, hot-swaps its signing material, and fans out
           `POST /v1/internal/renew-cert` to all configured internal DMS services.
        5. Hot-reloads `globalCryptoManager` in DMS-Core, transitioning from Setup Mode to Production.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LicenseUploadPayload'
      responses:
        '202':
          description: License accepted — ICA renewal fan-out triggered
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: "LICENSE_APPLIED" }
                  message: { type: string }
        '400':
          description: Malformed payload
        '403':
          description: Signature or decryption failed — FermiHDI Root CA chain validation error

  /v1/ca/crl/refresh:
    post:
      summary: Force CRL Refresh
      description: |
        Instructs the DMS-CA to regenerate and rebroadcast the Certificate Revocation List (CRL).
        Useful after bulk certificate revocations.
      security:
        - mtls: []
      responses:
        '202':
          description: CRL refresh triggered
        '500':
          description: CA unavailable or CRL generation failed

  /v1/telemetry/optin:
    post:
      summary: Toggle Analytics Opt-In
      description: Enables or disables voluntary telemetry upload to FermiHDI analytics.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean }
      responses:
        '200':
          description: Opt-in state updated

  /v1/dms/schema/update:
    post:
      summary: Update Active Dataset Schema
      description: |
        Broadcasts a new dataset bit-field schema to all registered HD Nodes and the DMS-OPA-Engine.
        The schema defines valid field names, types, and positions for use in OPA policy validation.
      security:
        - mtls: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                schema: { type: object, description: "New JSON schema definition" }
      responses:
        '202':
          description: Schema update broadcast initiated

  /v1/summary:
    get:
      summary: Cluster Health Summary
      description: Aggregated metrics for the dashboard.
      security:
        - mtls: []
      responses:
        '200':
          description: Health summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_nodes: { type: integer }
                  active_nodes: { type: integer }
                  healthy_nodes: { type: integer }
                  total_storage_mb: { type: number }
                  used_storage_mb: { type: number }
                  avg_cpu_percent: { type: number }
                  queries_serviced: { type: integer }

components:
  securitySchemes:
    mtls:
      type: mutualTLS
  schemas:
    NodeConfig:
      type: object
      properties:
        sc_status: { type: string, description: "Logical deployment state inherited by nodes attached to an SC target." }
        storage_strategy: { type: string }
        alert_thresholds:
          type: object
          additionalProperties: { type: number }
        events:
          type: array
          items: { type: string }
        opt_in: { type: boolean }
        sentry_dsn: { type: string }
        webhooks:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              events:
                type: array
                items: { type: string }
              headers:
                type: object
                additionalProperties: { type: string }

        # Storage Node Specific
        node_name: { type: string }
        dms_url: { type: string }
        command_port: { type: integer }
        command_interface: { type: string }
        observability_port: { type: integer }
        observability_interface: { type: string }
        log_level: { type: string }
        otel_grpc_url: { type: string }
        production_interface: { type: string }
        production_ip: { type: string }
        production_port: { type: integer }
        scc_ip:
          type: string
          description: "SCC control-plane IP injected by DMS into SN configs for hash reporting"
        scc_port:
          type: integer
          description: "SCC command port for SN→SCC communication"
        storage_devices:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              address: { type: string }
              type: { type: string }
        drive_strategy: { type: string }
        drive_full_action: { type: string }
        hash_offload_platform: { type: string }
        filter_device_selector: { type: string }
        use_ocf: { type: boolean }
        
        # SCC Specific
        record_size: { type: integer }
        max_hash_queue_bytes: { type: integer }
        batch_timeout_ms: { type: integer }
        timeout_action: { type: string, enum: [EOB, BBN] }
        sn_targets: { type: string }
        master_cpuset: { type: string }
        transport_cores: { type: integer }
        storage_cores: { type: integer }
        hash_cores: { type: integer }
        kv_cores: { type: integer }
        filter_cores: { type: integer }

        # Ingester Specific
        ingression_type: { type: string }
        input_format: { type: string }
        schema_file: { type: string }
        batch_interval_seconds: { type: integer, nullable: true }
        batch_id_field: { type: string }
        round_batch_id_field: { type: boolean }
        tcp_max_session_duration_sec: { type: integer }
        env: { type: string }
        otel_collector_url: { type: string }
        scc_targets:
          type: array
          items:
            type: object
            properties:
              address: { type: string }
              port: { type: integer }
              description: { type: string }

    HDInstance:
      type: object
      required: [instance_id, name]
      properties:
        instance_id: { type: string }
        name: { type: string }
        description: { type: string }
        status: { type: string, enum: [Active, Suspended] }
        created_at: { type: string, format: date-time }
        dataset_schema: { type: string, description: "Raw JSON string representing the dataset schema" }
        storage_clusters:
          type: array
          items: { type: string }

    RegisteredNode:
      type: object
      properties:
        node_id: { type: string }
        node_type: { type: string }
        instance_id: { type: string }
        status: { type: string, enum: [Provisioned, Active, FullyReady, Inactive, "Pending Approval", Offline, DRAINING] }
        registered_at: { type: string, format: date-time }
        last_seen: { type: string, format: date-time }
        latest_telemetry:
          type: object
          properties:
            health_status: { type: string }
            cpu_utilization: { type: number }
            mem_utilization: { type: number }
            queries_serviced: { type: integer }
            records_serviced: { type: integer }
            mb_processed: { type: number }
        system_info:
          type: object
          properties:
            hostname: { type: string }
            os: { type: string }
            arch: { type: string }
            kernel_version: { type: string }
        config:
          $ref: '#/components/schemas/NodeConfig'

    LicenseUploadPayload:
      type: object
      required: [licenseEncrypted, nonce, dmsCRT]
      properties:
        licenseEncrypted: { type: string, description: "Base64 encrypted payload" }
        nonce: { type: string, description: "Base64 nonce for AES-GCM" }
        dmsCRT: { type: string, description: "Signed DMS Certificate" }

    LicenseFile:
      type: object
      properties:
        version: { type: integer }
        client:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
        licenses:
          type: array
          items:
            type: object
            properties:
              serialNumber: { type: string }
              expire_date: { type: integer }
              elements:
                type: array
                items:
                  type: object
                  properties:
                    elementType: { type: string }
                    count: { type: integer }
