openapi: 3.1.0
info:
  title: Molfex API
  version: "1.0.0"
  summary: Host a file or static site on a molfex.app URL.
  description: |
    Authenticate with `Authorization: Bearer molfex_...`.
    Create a key at [studio.molfex.app/api-keys](https://studio.molfex.app/api-keys).
    There is no guest or email-only upload.

    ### Publish loop

    1. `POST /projects` — get `uploadUrl` and `contentType`
    2. `PUT` the file to `uploadUrl` (expires in one hour)
    3. `POST /projects/{id}/publish`
    4. Poll `GET /projects/{id}` until `active` or `failed`

    `url` values are hostnames without `https://`. Open `https://{url}`.

    Publish uses credits. If you are out, buy more on the billing page.
  contact:
    url: https://studio.molfex.app
  license:
    name: Proprietary
servers:
  - url: https://studio.molfex.app/api/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Projects
    description: Create, publish, list, replace, and delete hosted projects.
paths:
  /projects:
    get:
      tags: [Projects]
      operationId: listProjects
      summary: List projects
      description: List sites on this Molfex account.
      responses:
        "200":
          description: Project list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "400":
          $ref: "#/components/responses/Error"
    post:
      tags: [Projects]
      operationId: createProject
      summary: Create a project
      description: |
        Creates a draft project and returns a presigned `uploadUrl`.
        PUT the file to that URL, then call publish.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProjectRequest"
            examples:
              publicZip:
                summary: Public zip site
                value:
                  slug: my-site
                  fileName: site.zip
              passwordSite:
                summary: Password-protected site
                value:
                  slug: locked-site
                  fileName: site.zip
                  siteSettings:
                    passwordProtected: true
                    password: secret
      responses:
        "200":
          description: Draft created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateProjectResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /projects/{id}:
    parameters:
      - $ref: "#/components/parameters/ProjectId"
    get:
      tags: [Projects]
      operationId: getProject
      summary: Get publish status
      description: Get publish status and hostname for a project.
      responses:
        "200":
          description: Status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectStatusResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Projects]
      operationId: deleteProject
      summary: Delete a project
      description: Delete a project and its hosted files.
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /projects/{id}/upload:
    post:
      tags: [Projects]
      operationId: prepareUpload
      summary: Replace files
      description: |
        Get a new presigned upload URL for an existing project.
        PUT the file, then call publish. `sizeBytes` must be a positive integer.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PrepareUploadRequest"
            example:
              fileName: site.zip
              sizeBytes: 2048
      responses:
        "200":
          description: Upload URL ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrepareUploadResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /projects/{id}/publish:
    post:
      tags: [Projects]
      operationId: publishProject
      summary: Publish
      description: |
        Start publishing after the file was uploaded to `uploadUrl`.
        Returns 202 when the job is running.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublishRequest"
            example:
              fileName: site.zip
      responses:
        "200":
          description: Already active or finished immediately
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublishResponse"
        "202":
          description: Publish job started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublishResponse"
              example:
                ok: true
                data:
                  status: processing
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: molfex
      description: API key from https://studio.molfex.app/api-keys. Starts with `molfex_`.
  parameters:
    ProjectId:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      description: Project id returned by create or list.
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            ok: false
            error: You need an API key
    NotFound:
      description: Project not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            ok: false
            error: Project not found
    Error:
      description: Validation or business error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
  schemas:
    ErrorBody:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          enum: [false]
        error:
          type: string
    OkResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          enum: [true]
    SiteSettings:
      type: object
      required: [passwordProtected]
      properties:
        passwordProtected:
          type: boolean
        password:
          type: string
          minLength: 4
          maxLength: 200
          description: Required when passwordProtected is true.
    CreateProjectRequest:
      type: object
      required: [slug, fileName]
      properties:
        slug:
          type: string
          pattern: "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
          description: Lowercase letters, numbers, and hyphens. Becomes {slug}.molfex.app.
          example: my-site
        fileName:
          type: string
          minLength: 1
          maxLength: 255
          example: site.zip
        siteSettings:
          $ref: "#/components/schemas/SiteSettings"
    CreateProjectResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [projectId, uploadUrl, contentType, url]
          properties:
            projectId:
              type: integer
            uploadUrl:
              type: string
              format: uri
              description: Presigned PUT URL. Expires in one hour.
            contentType:
              type: string
              description: Send this exact Content-Type on the PUT.
            url:
              type: string
              description: Hostname without scheme.
              example: my-site.molfex.app
    ProjectListItem:
      type: object
      required: [id, slug, status, createdAt, url, customDomains, passwordProtected]
      properties:
        id:
          type: integer
        slug:
          type: string
        status:
          $ref: "#/components/schemas/ProjectStatus"
        createdAt:
          type: string
          format: date-time
        url:
          type: string
          example: my-site.molfex.app
        customDomains:
          type: array
          items:
            type: string
        passwordProtected:
          type: boolean
    ProjectListResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: array
          items:
            $ref: "#/components/schemas/ProjectListItem"
    ProjectStatus:
      type: string
      enum: [draft, processing, active, paused, failed]
    ProjectStatusResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [status, url, error]
          properties:
            status:
              $ref: "#/components/schemas/ProjectStatus"
            url:
              type: string
              example: my-site.molfex.app
            error:
              type: ["string", "null"]
    PrepareUploadRequest:
      type: object
      required: [fileName, sizeBytes]
      properties:
        fileName:
          type: string
          minLength: 1
        sizeBytes:
          type: integer
          minimum: 1
    PrepareUploadResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [projectId, uploadUrl, contentType, fileName]
          properties:
            projectId:
              type: integer
            uploadUrl:
              type: string
              format: uri
            contentType:
              type: string
            fileName:
              type: string
    PublishRequest:
      type: object
      required: [fileName]
      properties:
        fileName:
          type: string
          minLength: 1
    PublishResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [status]
          properties:
            status:
              type: string
              enum: [processing, active]
            url:
              type: string
