openapi: 3.0.3
info:
  title: FastVM Public API
  version: 1.0.0
  license:
    name: Proprietary
  description: |
    User-facing API for VM lifecycle, snapshots, console access, billing, quotas, and organization API key management.

    Authentication:
    - Supply `X-API-Key: <key>` for API key auth, or
    - Supply `Authorization: Bearer <jwt>` for JWT auth.
servers:
  - url: https://api.fastvm.org
    description: Production API endpoint
paths:
  /healthz:
    get:
      operationId: getHealthz
      tags: [System]
      summary: Health check
      security: []
      responses:
        "200":
          description: Healthy
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"

  /livez:
    get:
      operationId: getLivez
      tags: [System]
      summary: Liveness check
      security: []
      responses:
        "200":
          description: Live

  /readyz:
    get:
      operationId: getReadyz
      tags: [System]
      summary: Readiness check
      security: []
      responses:
        "200":
          description: Ready
        "503":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms:
    get:
      operationId: listVMs
      tags: [VMs]
      summary: List VMs for the authenticated organization
      responses:
        "200":
          description: VM list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/VMInstance"
        "401":
          $ref: "#/components/responses/ErrorResponse"
    post:
      operationId: createVM
      tags: [VMs]
      summary: Create VM from a base image or snapshot
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateVMRequest"
      responses:
        "201":
          description: VM created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "202":
          description: VM accepted and queued for capacity or worker readiness
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms/{id}:
    get:
      operationId: getVM
      tags: [VMs]
      summary: Get a single VM by id
      parameters:
        - $ref: "#/components/parameters/VMId"
      responses:
        "200":
          description: VM details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
    patch:
      operationId: updateVM
      tags: [VMs]
      summary: Rename a VM
      parameters:
        - $ref: "#/components/parameters/VMId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateVMRequest"
      responses:
        "200":
          description: Updated VM
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
    delete:
      operationId: deleteVM
      tags: [VMs]
      summary: Delete a VM
      parameters:
        - $ref: "#/components/parameters/VMId"
      responses:
        "200":
          description: VM deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms/{id}/firewall:
    put:
      operationId: replaceVMFirewall
      tags: [VMs]
      summary: Replace the IPv6 ingress firewall policy for a VM
      description: |
        Replaces the VM's public IPv6 ingress policy. This does not affect the internal IPv4 path used by platform control and exec.
      parameters:
        - $ref: "#/components/parameters/VMId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FirewallPolicy"
      responses:
        "200":
          description: Updated VM
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"
    patch:
      operationId: patchVMFirewall
      tags: [VMs]
      summary: Patch the IPv6 ingress firewall policy for a VM
      description: |
        Partially updates the VM's public IPv6 ingress policy. This does not affect the internal IPv4 path used by platform control and exec.
      parameters:
        - $ref: "#/components/parameters/VMId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchVMFirewallRequest"
      responses:
        "200":
          description: Updated VM
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VMInstance"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms/{id}/console-token:
    post:
      operationId: createVMConsoleToken
      tags: [Console]
      summary: Issue one-time token for websocket console access
      parameters:
        - $ref: "#/components/parameters/VMId"
      responses:
        "200":
          description: Console token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConsoleTokenResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms/{id}/exec:
    post:
      operationId: execVM
      tags: [VMs]
      summary: Execute a one-off command inside a running VM
      parameters:
        - $ref: "#/components/parameters/VMId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecVMRequest"
      responses:
        "200":
          description: Command completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecVMResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"

  /v1/vms/{id}/console/ws:
    get:
      operationId: connectVMConsoleWebsocket
      tags: [Console]
      summary: WebSocket console proxy
      description: |
        Upgrade this request to WebSocket and provide `session` query param from `/v1/vms/{id}/console-token`.
      security: []
      parameters:
        - $ref: "#/components/parameters/VMId"
        - name: session
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: HTTP response before protocol upgrade (non-WebSocket clients)
        "101":
          description: WebSocket upgrade successful
        "426":
          $ref: "#/components/responses/ErrorResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"

  /v1/snapshots:
    post:
      operationId: createSnapshot
      tags: [Snapshots]
      summary: Create a snapshot from a VM id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSnapshotRequest"
      responses:
        "201":
          description: Snapshot created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotObject"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "502":
          $ref: "#/components/responses/ErrorResponse"
    get:
      operationId: listSnapshots
      tags: [Snapshots]
      summary: List snapshots for the authenticated organization
      responses:
        "200":
          description: Snapshot list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SnapshotObject"
        "401":
          $ref: "#/components/responses/ErrorResponse"

  /v1/snapshots/{id}:
    patch:
      operationId: updateSnapshot
      tags: [Snapshots]
      summary: Rename a snapshot
      parameters:
        - $ref: "#/components/parameters/SnapshotId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSnapshotRequest"
      responses:
        "200":
          description: Updated snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotObject"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
    delete:
      operationId: deleteSnapshot
      tags: [Snapshots]
      summary: Delete a snapshot
      parameters:
        - $ref: "#/components/parameters/SnapshotId"
      responses:
        "200":
          description: Snapshot deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"

  /v1/org/quotas:
    get:
      operationId: getOrgQuotas
      tags: [Organization]
      summary: Get organization quota limits and current usage
      responses:
        "200":
          description: Organization quotas and usage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgQuotaUsage"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key auth (`hlm_...`).
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    VMId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    SnapshotId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    ErrorResponse:
      description: Error response
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string

    VMStatus:
      type: string
      enum: [provisioning, running, stopped, deleting, error]

    SnapshotStatus:
      type: string
      enum: [creating, ready, error]

    MachineType:
      type: string
      enum: [c1m2, c2m4, c4m8, c8m16]

    VMInstance:
      type: object
      required:
        - id
        - name
        - orgId
        - machineName
        - cpu
        - memoryMiB
        - diskGiB
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 64
        orgId:
          type: string
        machineName:
          type: string
        sourceName:
          type: string
        firewall:
          $ref: "#/components/schemas/FirewallPolicy"
          description: |
            Public IPv6 ingress firewall policy. If omitted for a newly created VM, the default is `restricted` with no ingress rules.
        cpu:
          type: integer
          minimum: 1
        memoryMiB:
          type: integer
          minimum: 1
        diskGiB:
          type: integer
          minimum: 10
        publicIpv6:
          type: string
        status:
          $ref: "#/components/schemas/VMStatus"
        createdAt:
          type: string
          format: date-time
        deletedAt:
          type: string
          format: date-time
          nullable: true

    SnapshotObject:
      type: object
      required:
        - id
        - name
        - orgId
        - vmId
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 64
        orgId:
          type: string
        vmId:
          type: string
          format: uuid
        firewall:
          $ref: "#/components/schemas/FirewallPolicy"
          description: Public IPv6 ingress firewall policy applied to the VM.
        status:
          $ref: "#/components/schemas/SnapshotStatus"
        createdAt:
          type: string
          format: date-time

    CreateVMRequest:
      type: object
      oneOf:
        - required: [machineType]
        - required: [snapshotId]
      properties:
        name:
          type: string
          maxLength: 64
        machineType:
          $ref: "#/components/schemas/MachineType"
        snapshotId:
          type: string
          format: uuid
        diskGiB:
          type: integer
          minimum: 10
          description: Optional grow-only disk size in GiB. Must be >= base machine disk (10 GiB) or >= source snapshot VM disk.
        firewall:
          $ref: "#/components/schemas/FirewallPolicy"
          description: Public IPv6 ingress firewall policy captured from the source VM at snapshot time.

    CreateSnapshotRequest:
      type: object
      required: [vmId]
      properties:
        vmId:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 64

    UpdateVMRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 64

    FirewallRule:
      type: object
      required: [protocol, portStart]
      description: A single allow rule for public IPv6 ingress.
      properties:
        protocol:
          type: string
          enum: [tcp, udp]
        portStart:
          type: integer
          minimum: 1
          maximum: 65535
        portEnd:
          type: integer
          minimum: 1
          maximum: 65535
        sourceCidrs:
          type: array
          description: IPv6 CIDRs allowed by this rule. If omitted, the backend treats the rule as open to `::/0`.
          items:
            type: string
        description:
          type: string
          maxLength: 256

    FirewallPolicy:
      type: object
      required: [mode]
      description: Public IPv6 ingress firewall policy for a VM or snapshot.
      properties:
        mode:
          type: string
          enum: [open, restricted]
        ingress:
          type: array
          description: |
            Allow rules evaluated only when `mode` is `restricted`. If empty, all public IPv6 ports are closed except essential ICMPv6 control traffic.
          items:
            $ref: "#/components/schemas/FirewallRule"

    PatchVMFirewallRequest:
      type: object
      description: Partial update payload for a VM's public IPv6 ingress firewall policy.
      properties:
        mode:
          type: string
          enum: [open, restricted]
        ingress:
          type: array
          items:
            $ref: "#/components/schemas/FirewallRule"

    ExecVMRequest:
      type: object
      required: [command]
      properties:
        command:
          type: array
          minItems: 1
          items:
            type: string
        timeoutSec:
          type: integer
          minimum: 1

    ExecVMResponse:
      type: object
      required:
        - exitCode
        - stdout
        - stderr
        - timedOut
        - stdoutTruncated
        - stderrTruncated
        - durationMs
      properties:
        exitCode:
          type: integer
        stdout:
          type: string
        stderr:
          type: string
        timedOut:
          type: boolean
        stdoutTruncated:
          type: boolean
        stderrTruncated:
          type: boolean
        durationMs:
          type: integer
          minimum: 0

    UpdateSnapshotRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 64

    ConsoleTokenResponse:
      type: object
      required: [token, expiresInSec, websocketPath]
      properties:
        token:
          type: string
        expiresInSec:
          type: integer
        websocketPath:
          type: string

    DeleteResponse:
      type: object
      required: [id, deleted]
      properties:
        id:
          type: string
        deleted:
          type: boolean

    OrgQuotaValues:
      type: object
      required: [vcpu, memoryMiB, diskGiB, snapshotCount]
      properties:
        vcpu:
          type: integer
          minimum: 0
        memoryMiB:
          type: integer
          minimum: 0
        diskGiB:
          type: integer
          minimum: 0
        snapshotCount:
          type: integer
          minimum: 0

    OrgQuotaUsage:
      type: object
      required: [orgId, limits, usage]
      properties:
        orgId:
          type: string
        limits:
          $ref: "#/components/schemas/OrgQuotaValues"
        usage:
          $ref: "#/components/schemas/OrgQuotaValues"

security:
  - ApiKeyAuth: []
  - BearerAuth: []
