openapi: 3.1.0
info:
  title: ADAMANT IPFS Node API
  version: 0.1.0
  description: >-
    Stable client, lifecycle, and node-health contract. Low-level Helia,
    libp2p, and optional debug operator routes are intentionally excluded.
servers:
  - url: https://ipfs.example.org
paths:
  /api/node/health:
    get:
      summary: Read the network-aware node checkpoint
      responses:
        '200':
          description: The current state; inspect the state field because this route is always 200
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /api/node/info:
    get:
      summary: Read sanitized legacy PWA and iOS node information
      responses:
        '200':
          description: Legacy node information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyNodeInfo'
  /api/node/details:
    get:
      summary: Read detailed operator state
      security:
        - AdminApiKey: []
      responses:
        '200':
          description: Node identity, storage, health, and bounded HTTP metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NodeDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/AdminNotConfigured'
  /api/file/upload:
    post:
      summary: Upload one or more files
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        '200':
          description: Files were stored and pinned
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          description: Request or file limit exceeded
        '429':
          description: Upload rate or concurrency limit exceeded
        '503':
          description: Required replication quorum was not reached
        '507':
          description: The disk reserve would be consumed
  /api/file/{cid}:
    parameters:
      - $ref: '#/components/parameters/Cid'
    get:
      summary: Download a complete content-addressed file
      description: Range headers are ignored and return the complete 200 representation
      responses:
        '200':
          description: Complete file attachment
          headers:
            ETag:
              schema: { type: string }
            Cache-Control:
              schema: { type: string }
            Accept-Ranges:
              schema: { type: string, const: none }
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '304':
          description: The validated CID representation has not changed
        '400':
          $ref: '#/components/responses/BadRequest'
        '408':
          description: Discovery or complete retrieval timed out
        '429':
          description: Read rate, per-client, or global download concurrency limit exceeded
  /api/file/{cid}/status:
    parameters:
      - $ref: '#/components/parameters/Cid'
    get:
      summary: Read public lifecycle and replication counts
      responses:
        '200': { description: File lifecycle state }
        '404': { description: Unknown CID }
  /api/file/{cid}/confirm:
    parameters:
      - $ref: '#/components/parameters/Cid'
    post:
      summary: Promote a temporary file to durable storage
      security:
        - AdminApiKey: []
      responses:
        '200': { description: File confirmed }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Unknown CID }
  /api/file/{cid}/unpin:
    parameters:
      - $ref: '#/components/parameters/Cid'
    post:
      summary: Release durable content
      security:
        - AdminApiKey: []
      responses:
        '200': { description: File released for later collection }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Unknown CID }
  /api/storage/metrics:
    get:
      summary: Read public bounded storage and replication counts
      responses:
        '200': { description: Cached storage metrics }
  /api/storage/policy:
    get:
      summary: Read upload, lifecycle, and durability policy
      responses:
        '200': { description: Public storage policy }
  /api/storage/gc:
    post:
      summary: Run or preview garbage collection
      security:
        - AdminApiKey: []
      parameters:
        - name: dryRun
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200': { description: Garbage collection report }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { description: Garbage collection is already running }
  /api/storage/repair:
    post:
      summary: Run the next bounded replication repair batch
      security:
        - AdminApiKey: []
      responses:
        '200': { description: Repair batch and cycle progress }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { description: Replication repair is already running }
components:
  securitySchemes:
    AdminApiKey:
      type: apiKey
      in: header
      name: x-api-key
  parameters:
    Cid:
      name: cid
      in: path
      required: true
      schema: { type: string }
  schemas:
    Health:
      type: object
      required:
        - version
        - uptimeMs
        - state
        - height
        - timestamp
        - evaluatedAt
        - checkpoint
        - membership
        - startup
        - storage
        - replication
        - checks
      properties:
        version: { type: string }
        uptimeMs: { type: integer, minimum: 0 }
        state:
          type: string
          enum: [starting, ready, stale, degraded]
          description: >-
            Downgrades happen at read time; recovery to ready needs a successful
            checkpoint, so expect up to checkpoint.intervalMs of lag. Stale only
            after checkpoint age exceeds maxAgeMs; the exact limit is valid
        height:
          type: integer
          minimum: 0
          description: >-
            Persisted Unix milliseconds at the start of the last valid fixed round.
            Peers may attest an adjacent round across a boundary, so two healthy
            nodes can report heights one intervalMs apart at a round change
        timestamp: { type: integer, description: When this response was produced }
        evaluatedAt:
          type: integer
          description: >-
            When the checkpoint attempt behind this snapshot ran; membership,
            startup, and the non-age checks are dated by this, not by checkpoint.observedAt
        checkpoint:
          type: object
          required: [intervalMs, observedAt, ageMs, maxAgeMs]
          properties:
            intervalMs: { type: integer, minimum: 1000 }
            observedAt: { type: [integer, 'null'] }
            ageMs: { type: [integer, 'null'], minimum: 0 }
            maxAgeMs: { type: integer, minimum: 1000 }
        membership:
          type: object
          description: Peer evidence from the attempt at evaluatedAt, which may be a failed one
          required: [version, requiredPeers, attestedPeers]
          properties:
            version:
              type: string
              pattern: '^[a-f0-9]{64}$'
              description: Peer-set epoch; compare checkpoint heights only when versions match
            requiredPeers: { type: integer, minimum: 0 }
            attestedPeers: { type: integer, minimum: 0 }
        startup:
          type: object
          description: Startup evidence from the attempt at evaluatedAt, which may be a failed one
          required: [complete, healthy]
          properties:
            complete: { type: boolean }
            healthy: { type: boolean }
        storage:
          type: object
          required: [measuredAt, measurementAgeMs, reserveHealthy]
          properties:
            measuredAt: { type: [integer, 'null'] }
            measurementAgeMs: { type: [integer, 'null'], minimum: 0 }
            reserveHealthy: { type: boolean }
        replication:
          type: object
          required: [repairRequired, lastCompleteAt, ageMs, backlog]
          properties:
            repairRequired: { type: boolean }
            lastCompleteAt: { type: [integer, 'null'] }
            ageMs: { type: [integer, 'null'], minimum: 0 }
            backlog: { type: integer, minimum: 0 }
        checks:
          type: object
          description: Checkpoint, storage, and repair freshness are recomputed at read time; every other check describes evaluatedAt
          additionalProperties: { type: boolean }
    HttpMetrics:
      type: object
      description: Counters accumulated since process start, not a sliding window
      required:
        - requests
        - responses
        - aborted
        - inFlight
        - totalResponseTimeMs
        - maxResponseTimeMs
        - averageResponseTimeMs
      properties:
        requests: { type: integer, minimum: 0 }
        responses:
          type: object
          required: [2xx, 3xx, 4xx, 5xx]
          properties:
            2xx: { type: integer, minimum: 0 }
            3xx: { type: integer, minimum: 0 }
            4xx: { type: integer, minimum: 0 }
            5xx: { type: integer, minimum: 0 }
        aborted:
          type: integer
          minimum: 0
          description: Responses whose connection closed before the body finished, counted apart from the status families
        inFlight: { type: integer, minimum: 0 }
        totalResponseTimeMs: { type: number, minimum: 0 }
        maxResponseTimeMs: { type: number, minimum: 0 }
        averageResponseTimeMs: { type: number, minimum: 0 }
    NodeDetails:
      type: object
      required:
        - version
        - timestamp
        - heliaStatus
        - peerId
        - multiAddresses
        - blockstoreSizeMb
        - datastoreSizeMb
        - availableSizeInMb
        - pinnedBytes
        - reclaimableBytes
        - availableBytes
        - reservedBytes
        - health
        - http
        - concurrency
      properties:
        version: { type: string }
        timestamp: { type: integer }
        heliaStatus: { type: string }
        peerId: { type: string }
        multiAddresses:
          type: array
          items: { type: string }
        blockstoreSizeMb: { type: number }
        datastoreSizeMb: { type: number }
        availableSizeInMb: { type: number }
        pinnedBytes: { type: integer, minimum: 0 }
        reclaimableBytes: { type: integer, minimum: 0 }
        availableBytes: { type: integer, minimum: 0 }
        reservedBytes: { type: integer, minimum: 0 }
        health: { $ref: '#/components/schemas/Health' }
        http: { $ref: '#/components/schemas/HttpMetrics' }
        concurrency: { $ref: '#/components/schemas/ConcurrencyMetrics' }
    ConcurrencyMetrics:
      type: object
      description: Admission-limiter occupancy, so a capacity 429 is distinguishable from a rate-limit 429
      required: [uploads, incomingCopies, downloads]
      properties:
        uploads:
          type: object
          required: [active, limit]
          properties:
            active: { type: integer, minimum: 0 }
            limit: { type: integer, minimum: 1 }
        incomingCopies:
          type: object
          required: [active, limit]
          properties:
            active: { type: integer, minimum: 0 }
            limit: { type: integer, minimum: 1 }
        downloads:
          type: object
          required: [active, limit, perClientLimit]
          properties:
            active: { type: integer, minimum: 0 }
            limit: { type: integer, minimum: 1 }
            perClientLimit: { type: integer, minimum: 1 }
    LegacyNodeInfo:
      type: object
      required:
        - version
        - timestamp
        - heliaStatus
        - blockstoreSizeMb
        - datastoreSizeMb
        - availableSizeInMb
      properties:
        version: { type: string }
        timestamp: { type: integer }
        heliaStatus: { type: string }
        blockstoreSizeMb: { type: number }
        datastoreSizeMb: { type: number }
        availableSizeInMb: { type: number }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Administrative API key is missing or invalid
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    AdminNotConfigured:
      description: Administrative API key is not configured
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
