openapi: 3.0.3
info:
  title: Hackless Public API
  version: 1.0.0
  description: Public REST API for Hackless challenge, leaderboard, profile, writeup, and flag submission workflows.
servers:
  - url: https://hackless.dev
    description: Production
security:
  - bearerAuth: []
  - apiKeyAuth: []
paths:
  /api/public/health:
    get:
      tags: [Health]
      summary: Health check
      security: []
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    example: true
  /api/public/challenges:
    get:
      tags: [Challenges]
      summary: List challenges
      security: []
      responses:
        "200":
          description: Challenge summaries
          content:
            application/json:
              schema:
                type: object
                required: [challenges]
                properties:
                  challenges:
                    type: array
                    items:
                      $ref: "#/components/schemas/ChallengeSummary"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/challenges/{slug}:
    get:
      tags: [Challenges]
      summary: Get challenge by slug
      security: []
      parameters:
        - $ref: "#/components/parameters/Slug"
      responses:
        "200":
          description: Challenge detail
          content:
            application/json:
              schema:
                type: object
                required: [challenge]
                properties:
                  challenge:
                    $ref: "#/components/schemas/ChallengeDetail"
        "404":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/challenges/{slug}/submit:
    post:
      tags: [Challenges]
      summary: Submit a flag
      parameters:
        - $ref: "#/components/parameters/Slug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [flag]
              properties:
                flag:
                  type: string
                  minLength: 1
                  example: hackless{example_flag}
      responses:
        "200":
          description: Submission accepted
          content:
            application/json:
              schema:
                type: object
                required: [result]
                properties:
                  result:
                    $ref: "#/components/schemas/SubmitFlagResult"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/challenges/{slug}/writeups:
    get:
      tags: [Writeups]
      summary: List challenge writeups
      parameters:
        - $ref: "#/components/parameters/Slug"
      responses:
        "200":
          description: Challenge writeups
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChallengeWriteupsResponse"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/leaderboard:
    get:
      tags: [Leaderboard]
      summary: Get leaderboard
      security: []
      responses:
        "200":
          description: Leaderboard entries
          content:
            application/json:
              schema:
                type: object
                required: [leaderboard]
                properties:
                  leaderboard:
                    type: array
                    items:
                      $ref: "#/components/schemas/LeaderboardEntry"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/me:
    get:
      tags: [Profiles]
      summary: Get authenticated profile
      responses:
        "200":
          description: Authenticated user profile
          content:
            application/json:
              schema:
                type: object
                required: [profile]
                properties:
                  profile:
                    $ref: "#/components/schemas/MyProfile"
        "401":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
  /api/public/profiles/{userId}:
    get:
      tags: [Profiles]
      summary: Get public profile
      security: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Public profile or null
          content:
            application/json:
              schema:
                type: object
                required: [profile]
                properties:
                  profile:
                    oneOf:
                      - $ref: "#/components/schemas/PublicProfile"
                      - type: "null"
        "500":
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Hackless MCP/API key, for example Authorization: Bearer hk_...
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    Slug:
      name: slug
      in: path
      required: true
      schema:
        type: string
      example: cryptovault
  responses:
    Error:
      description: Error response
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
    ChallengeDifficulty:
      type: string
      enum: [Easy, Medium, Hard, Insane]
    ChallengeCreator:
      type: object
      nullable: true
      properties:
        id:
          type: string
        name:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
    ChallengeSummary:
      type: object
      required: [id, slug, categories, title, description, difficulty, points, hasInstance, type, attemptsCount, createdAt, solved, solvesCount]
      properties:
        id: { type: string }
        slug: { type: string }
        categories:
          type: array
          items: { type: string }
        title: { type: string }
        description: { type: string }
        difficulty:
          $ref: "#/components/schemas/ChallengeDifficulty"
        points: { type: integer }
        fileUrl: { type: string, nullable: true }
        fileName: { type: string, nullable: true }
        hasInstance: { type: boolean }
        dockerPort: { type: integer, nullable: true }
        type: { type: string, example: web }
        attemptsCount: { type: integer }
        createdAt: { type: string, format: date-time }
        creator:
          $ref: "#/components/schemas/ChallengeCreator"
        solved: { type: boolean }
        solvesCount: { type: integer }
        avgStars: { type: number, nullable: true }
    ChallengeDetail:
      allOf:
        - $ref: "#/components/schemas/ChallengeSummary"
        - type: object
          required: [unlocked, submittedFlag, totalReviews]
          properties:
            unlocked: { type: boolean }
            submittedFlag: { type: string, nullable: true }
            avgDifficulty: { type: number, nullable: true }
            totalReviews: { type: integer }
            myReview:
              $ref: "#/components/schemas/ChallengeReview"
              nullable: true
    ChallengeReview:
      type: object
      required: [stars, difficulty]
      properties:
        stars: { type: integer, minimum: 1, maximum: 5 }
        difficulty: { type: integer, minimum: 1, maximum: 10 }
    SubmitFlagResult:
      type: object
      required: [success, points, newBadges]
      properties:
        success: { type: boolean }
        points: { type: integer }
        newBadges:
          type: array
          items:
            $ref: "#/components/schemas/BadgeAward"
    BadgeAward:
      type: object
      required: [id, title, icon, image]
      properties:
        id: { type: string }
        title: { type: string }
        icon: { type: string }
        image: { type: string, nullable: true }
    LeaderboardEntry:
      type: object
      required: [id, name, totalPoints, totalSolves, solves]
      properties:
        id: { type: string }
        name: { type: string }
        username: { type: string, nullable: true }
        image: { type: string, nullable: true }
        totalPoints: { type: integer }
        totalSolves: { type: integer }
        lastSolvedAt: { type: string, format: date-time, nullable: true }
        solves:
          type: array
          items:
            $ref: "#/components/schemas/LeaderboardSolve"
    LeaderboardSolve:
      type: object
      required: [solvedAt, points]
      properties:
        solvedAt: { type: string, format: date-time }
        points: { type: integer }
    PublicProfile:
      type: object
      required: [id, name, username, image, createdAt, solvedChallenges, createdChallenges, badges]
      properties:
        id: { type: string }
        name: { type: string, nullable: true }
        username: { type: string, nullable: true }
        image: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        solvedChallenges:
          type: array
          items:
            $ref: "#/components/schemas/ProfileSolve"
        createdChallenges:
          type: array
          items:
            $ref: "#/components/schemas/ProfileChallenge"
        badges:
          type: array
          items:
            $ref: "#/components/schemas/ProfileBadge"
        rank: { type: integer }
        totalPoints: { type: integer }
        firstBloodCount: { type: integer }
        top3Count: { type: integer }
        categoryCounts:
          type: object
          additionalProperties: { type: integer }
        difficultyCounts:
          type: object
          additionalProperties: { type: integer }
    MyProfile:
      allOf:
        - $ref: "#/components/schemas/PublicProfile"
        - type: object
          required: [email, twoFactorEnabled, rank, totalPoints, firstBloodCount, top3Count]
          properties:
            email: { type: string, format: email, nullable: true }
            twoFactorEnabled: { type: boolean }
            mcpApiKeys:
              type: array
              items:
                $ref: "#/components/schemas/McpApiKey"
    ProfileSolve:
      type: object
      required: [solvedAt, challenge]
      properties:
        solvedAt: { type: string, format: date-time }
        challenge:
          $ref: "#/components/schemas/ProfileChallenge"
    ProfileChallenge:
      type: object
      required: [id, title, points, categories, difficulty]
      properties:
        id: { type: string }
        slug: { type: string }
        title: { type: string }
        points: { type: integer }
        categories:
          type: array
          items: { type: string }
        difficulty:
          $ref: "#/components/schemas/ChallengeDifficulty"
        createdAt: { type: string, format: date-time }
    ProfileBadge:
      type: object
      required: [badgeId, awardedAt]
      properties:
        badgeId: { type: string }
        awardedAt: { type: string, format: date-time }
    McpApiKey:
      type: object
      required: [id, name, keyPrefix, createdAt]
      properties:
        id: { type: string }
        name: { type: string }
        keyPrefix: { type: string }
        createdAt: { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        revokedAt: { type: string, format: date-time, nullable: true }
    ChallengeWriteupsResponse:
      type: object
      required: [challenge, writeups]
      properties:
        challenge:
          type: object
          required: [id, title, categories, difficulty, points]
          properties:
            id: { type: string }
            title: { type: string }
            categories:
              type: array
              items: { type: string }
            difficulty:
              $ref: "#/components/schemas/ChallengeDifficulty"
            points: { type: integer }
        writeups:
          type: array
          items:
            $ref: "#/components/schemas/Writeup"
    Writeup:
      type: object
      properties:
        id: { type: string }
        userId: { type: string }
        challengeId: { type: string }
        title: { type: string }
        content: { type: string }
        images:
          type: array
          items: { type: string }
        status: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        user:
          type: object
          properties:
            username: { type: string, nullable: true }
            name: { type: string, nullable: true }
