{
  "openapi": "3.1.0",
  "info": {
    "title": "fuush API",
    "description": "Deploy and manage static sites on fuu.sh. Authenticate with a Bearer token from https://app.fuu.sh/tokens (or via the device flow below). An MCP server with the same capabilities lives at https://api.fuu.sh/mcp.",
    "version": "1.0.0",
    "contact": {"email": "support@fuu.sh"}
  },
  "servers": [{"url": "https://api.fuu.sh"}],
  "security": [{"bearerAuth": []}],
  "paths": {
    "/v1/whoami": {
      "get": {
        "operationId": "whoami",
        "summary": "Validate the token and return its owner",
        "responses": {
          "200": {"description": "Token is valid", "content": {"application/json": {"schema": {"type": "object", "properties": {"user_id": {"type": "string", "format": "uuid"}, "email": {"type": "string"}}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/v1/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List the sites this token can deploy to",
        "responses": {
          "200": {"description": "Site list", "content": {"application/json": {"schema": {"type": "object", "properties": {"sites": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "slug": {"type": "string"}, "url": {"type": "string"}, "size_bytes": {"type": "integer"}, "is_active": {"type": "boolean"}, "spa_mode": {"type": "boolean"}, "updated_at": {"type": "string", "format": "date-time"}}}}}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/v1/sites/{slug}/deploy": {
      "post": {
        "operationId": "deploySite",
        "summary": "Deploy a zip to a site",
        "description": "Multipart upload, field name 'file'. The zip must contain the site's index file at its root. Max 100 MB compressed and uncompressed. Static content only; server-side code never executes. Deploys are atomic and safe to retry; every deploy is a release you can roll back from the dashboard.",
        "parameters": [{"name": "slug", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The site's subdomain label (<slug>.fuu.sh)"}],
        "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"type": "object", "required": ["file"], "properties": {"file": {"type": "string", "format": "binary", "description": "The site zip"}}}}}},
        "responses": {
          "200": {"description": "Deployed", "content": {"application/json": {"schema": {"type": "object", "properties": {"site_id": {"type": "string", "format": "uuid"}, "release_id": {"type": "string", "format": "uuid"}, "site_url": {"type": "string", "description": "The live URL, e.g. https://myslug.fuu.sh"}, "size_bytes": {"type": "integer"}}}}}},
          "400": {"description": "Rejected zip. error.code is one of: zip_slip, zip_bomb, banned_extension, zip_empty", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"description": "Token's owner does not own this site, or the token is site-scoped and this site is not in its allowlist (error.code: token_not_scoped)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "404": {"description": "No site with this slug", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "413": {"description": "Zip exceeds 100 MB", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "429": {"description": "Upload rate limit (10/hour/user)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/v1/device/start": {
      "post": {
        "operationId": "deviceStart",
        "summary": "Begin device authorization (get a token without pasting one)",
        "description": "No auth required. Returns a user_code for a human to approve at verification_uri while the client polls /v1/device/poll.",
        "security": [],
        "responses": {
          "200": {"description": "Device authorization started", "content": {"application/json": {"schema": {"type": "object", "properties": {"device_code": {"type": "string"}, "user_code": {"type": "string", "description": "Short code the human enters, e.g. XKCD-1234"}, "verification_uri": {"type": "string"}, "interval": {"type": "integer", "description": "Minimum seconds between polls"}, "expires_in": {"type": "integer"}}}}}}
        }
      }
    },
    "/v1/device/poll": {
      "post": {
        "operationId": "devicePoll",
        "summary": "Poll for the token after the human approves",
        "security": [],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["device_code"], "properties": {"device_code": {"type": "string"}}}}}},
        "responses": {
          "200": {"description": "Approved — token issued (returned exactly once)", "content": {"application/json": {"schema": {"type": "object", "properties": {"token": {"type": "string", "description": "Bearer token, fuu_..."}}}}}},
          "202": {"description": "Authorization still pending; poll again after 'interval' seconds"},
          "404": {"description": "Unknown or expired device_code", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "429": {"description": "Polling faster than 'interval'"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "description": "fuu_ token from https://app.fuu.sh/tokens or the device flow. Tokens may be site-scoped: a scoped token only sees/deploys its allowlisted sites; other sites return 403 token_not_scoped."}},
    "schemas": {"Error": {"type": "object", "properties": {"error": {"type": "object", "properties": {"code": {"type": "string"}, "message": {"type": "string"}}}}}},
    "responses": {"Unauthorized": {"description": "Missing or invalid Bearer token", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}
  }
}