{
  "openapi": "3.0.3",
  "info": {
    "title": "GiselleBot Edge Service - api-gateway",
    "description": "Public API gateway for cycloptux's bots: Discord OAuth2 + JWT authority, bot reverse proxy, service API, inbound webhook fan-out.",
    "version": "2.2.0"
  },
  "components": {
    "schemas": {
      "def-0": {
        "type": "object",
        "description": "Legacy problem+json error envelope returned by every gateway route on failure.",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "description": "HTTP reason name (e.g. \"NOT_FOUND\")."
                },
                "message": {
                  "type": "string",
                  "description": "Human-readable error message."
                }
              },
              "additionalProperties": true
            }
          }
        },
        "required": [
          "errors"
        ],
        "title": "ProblemJsonError"
      }
    }
  },
  "paths": {
    "/api/v1/static/website": {
      "get": {
        "operationId": "getStaticWebsiteData",
        "summary": "Fetch website static data",
        "tags": [
          "Static"
        ],
        "description": "Proxies the static website data JSON from S3, refreshing the embedded timestamp fields. Unauthenticated.",
        "responses": {
          "200": {
            "description": "Static website data with an injected timestamp",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Static website data with an injected timestamp",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "500": {
            "description": "Upstream fetch or processing error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bots": {
      "get": {
        "operationId": "getBotsForUser",
        "summary": "List bots available to the authenticated user",
        "tags": [
          "Bots"
        ],
        "description": "Returns the linked bots whose servers intersect the user's Discord guilds.",
        "responses": {
          "200": {
            "description": "Bots available to the user",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Bots available to the user",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error or Discord API failure",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bots/356831787445387285/{*}": {
      "get": {
        "operationId": "proxyGetBot356831787445387285",
        "summary": "Proxy Get requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "proxyPostBot356831787445387285",
        "summary": "Proxy Post requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "proxyPatchBot356831787445387285",
        "summary": "Proxy Patch requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "proxyDeleteBot356831787445387285",
        "summary": "Proxy Delete requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bots/531757157142495242/{*}": {
      "get": {
        "operationId": "proxyGetBot531757157142495242",
        "summary": "Proxy Get requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "proxyPostBot531757157142495242",
        "summary": "Proxy Post requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "proxyPatchBot531757157142495242",
        "summary": "Proxy Patch requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "proxyDeleteBot531757157142495242",
        "summary": "Proxy Delete requests to the bot",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request (path, query string, body where applicable) to the linked bot over its internal API, passing X-Bots-Key/X-Discord-ID/X-Guild-IDs headers and unwrapping single-element {data:[x]} responses. Response is an upstream passthrough.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream bot response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream bot response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error (passthrough where the bot supplied a problem+json body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bots/botwebhooks/356831787445387285/{*}": {
      "post": {
        "operationId": "proxyPostBotWebhooks356831787445387285",
        "summary": "Proxy POST requests to the bot webhook receiver",
        "tags": [
          "Bots"
        ],
        "description": "Forwards the request to the linked bot webhook receiver with the X-Bots-Key header, unwrapping single-element {data:[x]} responses.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "*",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Upstream response (passthrough)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Upstream response (passthrough)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Upstream error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services/shorturl/create": {
      "post": {
        "operationId": "createShortUrl",
        "summary": "Create a short URL",
        "tags": [
          "Services"
        ],
        "description": "Shortens a URL (optionally with a vanity ID) via the URL shortener microservice. Requires a valid service API key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The URL to shorten"
                  },
                  "vanity": {
                    "type": "string",
                    "description": "Optional vanity short ID"
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The shortened URL",
            "content": {
              "application/json": {
                "schema": {
                  "description": "The shortened URL",
                  "type": "object",
                  "properties": {
                    "short_url": {
                      "type": "string",
                      "description": "The short URL"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "URL missing or not a string",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "401": {
            "description": "API key was not supplied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "API key not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "415": {
            "description": "Short URL creation failed or URL scheme unsupported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services/nsfwjs/predict": {
      "post": {
        "operationId": "nsfwjsPredict",
        "summary": "Run NSFW prediction on an image URL",
        "tags": [
          "Services"
        ],
        "description": "Classifies the image at the given URL via the NSFWJS microservice. Requires a valid service API key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Image URL to classify"
                  },
                  "nsfw_threshold": {
                    "type": "number",
                    "description": "Optional custom NSFW threshold (0-1]"
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Prediction object (passthrough from the NSFWJS microservice)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Prediction object (passthrough from the NSFWJS microservice)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "URL missing or not a string",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "401": {
            "description": "API key was not supplied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "API key not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "415": {
            "description": "URL is not an image or scheme unsupported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services/nsfwjs/cache": {
      "post": {
        "operationId": "nsfwjsCache",
        "summary": "Fetch a cached NSFW prediction",
        "tags": [
          "Services"
        ],
        "description": "Fetches a previously cached NSFW prediction by its 24-hex cache ID via the NSFWJS microservice. Requires a valid service API key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cache_id": {
                    "type": "string",
                    "description": "24-hex MongoDB ObjectId of the cached prediction"
                  },
                  "nsfw_threshold": {
                    "type": "number",
                    "description": "Optional custom NSFW threshold (0-1]"
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Prediction object (passthrough from the NSFWJS microservice)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Prediction object (passthrough from the NSFWJS microservice)",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Cache ID missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "401": {
            "description": "API key was not supplied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "API key not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "404": {
            "description": "No cached image found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/user": {
      "get": {
        "operationId": "getDiscordUser",
        "summary": "Get the authenticated Discord user",
        "tags": [
          "Discord API"
        ],
        "description": "Returns the JWT-authenticated user's Discord profile, fetched from Discord with the user's stored OAuth token.",
        "responses": {
          "200": {
            "description": "Discord user profile subset",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Discord user profile subset",
                  "type": "object",
                  "properties": {
                    "username": {
                      "type": "string",
                      "description": "Discord username"
                    },
                    "discriminator": {
                      "type": "string",
                      "description": "Discord discriminator"
                    },
                    "id": {
                      "type": "string",
                      "description": "Discord user ID"
                    },
                    "avatar": {
                      "type": [
                        "null",
                        "string"
                      ],
                      "description": "Avatar hash"
                    },
                    "locale": {
                      "type": "string",
                      "description": "User locale"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Invalid or unauthorized token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error or Discord API failure",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/login": {
      "get": {
        "operationId": "initializeDiscordLogin",
        "summary": "Start the Discord OAuth2 login flow",
        "tags": [
          "Auth"
        ],
        "description": "Stores a state token for the caller (identified by the computed x-uuid) and redirects (307) to the Discord authorize URL. The redirect_uri query parameter must be an authorized callback.",
        "parameters": [
          {
            "schema": "object",
            "in": "query",
            "name": "type"
          },
          {
            "schema": true,
            "in": "query",
            "name": "additionalProperties"
          }
        ],
        "responses": {
          "403": {
            "description": "Invalid UUID, missing or unauthorized callback URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "State storage error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/callback": {
      "get": {
        "operationId": "handleDiscordCallback",
        "summary": "Discord OAuth2 callback",
        "tags": [
          "Auth"
        ],
        "description": "Exchanges the Discord authorization code, mints a gateway JWT, stores it under a temporary code and redirects to the original callback URL with that code.",
        "parameters": [
          {
            "schema": "object",
            "in": "query",
            "name": "type"
          },
          {
            "schema": true,
            "in": "query",
            "name": "additionalProperties"
          }
        ],
        "responses": {
          "403": {
            "description": "Missing code/state or Discord-side error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "408": {
            "description": "State token expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/token/revoke": {
      "post": {
        "operationId": "revokeJwtToken",
        "summary": "Revoke a gateway JWT",
        "tags": [
          "Auth"
        ],
        "description": "Revokes the presented JWT (jti added to the revocation list), deletes its refresh token, and revokes the underlying Discord OAuth tokens.",
        "responses": {
          "200": {
            "description": "Revocation outcome",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Revocation outcome",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Token missing or already revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "404": {
            "description": "Refresh token data not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Revocation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/token/refresh": {
      "post": {
        "operationId": "refreshJwtToken",
        "summary": "Refresh a gateway JWT",
        "tags": [
          "Auth"
        ],
        "description": "Exchanges a valid refresh token (x-refresh-token header) for a new JWT + refresh token pair, revoking the previous JWT and refreshing the underlying Discord token.",
        "responses": {
          "200": {
            "description": "Newly issued gateway JWT bundle",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Newly issued gateway JWT bundle",
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "Gateway JWT"
                    },
                    "token_type": {
                      "type": "string",
                      "description": "Always \"Bearer\""
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Seconds until the JWT expires"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Opaque refresh token"
                    },
                    "scope": {
                      "type": "string",
                      "description": "Space-separated scopes"
                    },
                    "issued_at": {
                      "type": "string",
                      "description": "ISO-8601 issue timestamp"
                    },
                    "expiration_date": {
                      "type": "string",
                      "description": "ISO-8601 expiration timestamp"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable status message"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Refresh token missing or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "404": {
            "description": "Refresh token data not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Refresh error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/token/check": {
      "post": {
        "operationId": "checkJwtToken",
        "summary": "Check a gateway JWT",
        "tags": [
          "Auth"
        ],
        "description": "Validates the presented JWT (signature, allowlist, revocation) and returns its validity and expiration metadata.",
        "responses": {
          "200": {
            "description": "Token validity info",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Token validity info",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Token missing or revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/discord/token": {
      "post": {
        "operationId": "obtainUserToken",
        "summary": "Exchange a temporary code for a JWT",
        "tags": [
          "Auth"
        ],
        "description": "Swaps the temporary code produced by the OAuth callback (plus the matching x-uuid) for the stored JWT bundle.",
        "responses": {
          "200": {
            "description": "Newly issued gateway JWT bundle",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Newly issued gateway JWT bundle",
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "Gateway JWT"
                    },
                    "token_type": {
                      "type": "string",
                      "description": "Always \"Bearer\""
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Seconds until the JWT expires"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Opaque refresh token"
                    },
                    "scope": {
                      "type": "string",
                      "description": "Space-separated scopes"
                    },
                    "issued_at": {
                      "type": "string",
                      "description": "ISO-8601 issue timestamp"
                    },
                    "expiration_date": {
                      "type": "string",
                      "description": "ISO-8601 expiration timestamp"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable status message"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing/expired code, UUID or state mismatch",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Storage error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/issuetracker/overview": {
      "get": {
        "operationId": "issueTrackerOverview",
        "summary": "Issue tracker overview counts and DeepSeek peak/off-peak clock",
        "tags": [
          "Issue Tracker"
        ],
        "description": "Returns issue counts by lifecycle state, by submitter severity, and by analysis state, plus the current DeepSeek peak/off-peak clock (which guides whether an analysis runs immediately or is deferred).",
        "responses": {
          "200": {
            "description": "Overview counts and clock.",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Overview counts and clock.",
                  "type": "object",
                  "properties": {
                    "byState": {
                      "type": "object",
                      "description": "Issue count per lifecycle state.",
                      "additionalProperties": {
                        "type": "number"
                      }
                    },
                    "bySeverity": {
                      "type": "object",
                      "description": "Issue count per submitter severity.",
                      "additionalProperties": {
                        "type": "number"
                      }
                    },
                    "byAnalysisState": {
                      "type": "object",
                      "description": "Issue count per analysis state.",
                      "additionalProperties": {
                        "type": "number"
                      }
                    },
                    "clock": {
                      "type": "object",
                      "description": "DeepSeek peak/off-peak pricing clock.",
                      "properties": {
                        "isPeak": {
                          "type": "boolean",
                          "description": "True when inside a peak-pricing window."
                        },
                        "secondsToNextBoundary": {
                          "type": "number",
                          "description": "Seconds until the next peak/off-peak boundary."
                        }
                      }
                    }
                  },
                  "required": [
                    "byState",
                    "bySeverity",
                    "byAnalysisState",
                    "clock"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Not a member of the issue tracker",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/issuetracker/worker-status": {
      "get": {
        "operationId": "issueTrackerWorkerStatus",
        "summary": "AI analysis worker availability",
        "tags": [
          "Issue Tracker"
        ],
        "description": "Probes the analysis worker health endpoint and returns whether the AI workspace is reachable. Guides whether an analysis will run soon or is deferred. The worker is a best-effort dependency - a false here means jobs queue and resume later, not that the tracker is broken.",
        "responses": {
          "200": {
            "description": "Worker availability.",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Worker availability.",
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean",
                      "description": "Whether the analysis worker health probe succeeded."
                    },
                    "checkedAt": {
                      "type": "string",
                      "description": "ISO timestamp of the probe."
                    }
                  },
                  "required": [
                    "available",
                    "checkedAt"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Not a member of the issue tracker",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/issuetracker/config": {
      "get": {
        "operationId": "issueTrackerGetConfig",
        "summary": "Get the full tracker configuration (issuetracker:config-only)",
        "tags": [
          "Issue Tracker"
        ],
        "description": "Returns the curated runtime configuration: the bottom-up triage config (whether the error-ingestion path is enabled, which services intercept error events, dedup TTL, minimum severity to open, auto-publish) and the model routing config (flash/pro tier chains, tier cooldowns, pro-review confidence threshold, peak windows, vision model). Gated on the issuetracker:config role specifically - admin does not imply this.",
        "responses": {
          "200": {
            "description": "Full tracker configuration (bottom-up + model).",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Full tracker configuration (bottom-up + model).",
                  "type": "object",
                  "properties": {
                    "config": {
                      "type": "object",
                      "description": "Bottom-up triage configuration.",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "services": {
                          "type": "object",
                          "properties": {
                            "bot": {
                              "type": "boolean"
                            },
                            "api-gateway": {
                              "type": "boolean"
                            },
                            "webhooks": {
                              "type": "boolean"
                            },
                            "discord-proxy": {
                              "type": "boolean"
                            },
                            "microservices": {
                              "type": "boolean"
                            }
                          }
                        },
                        "dedupTtlDays": {
                          "type": "number"
                        },
                        "minSeverityToOpen": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ]
                        },
                        "autoOpen": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "enabled",
                        "services",
                        "dedupTtlDays",
                        "minSeverityToOpen",
                        "autoOpen"
                      ]
                    },
                    "model": {
                      "type": "object",
                      "description": "Model routing configuration.",
                      "properties": {
                        "flashTiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "providerId": {
                                "type": "string"
                              },
                              "modelId": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "peakExempt": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "proTiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "providerId": {
                                "type": "string"
                              },
                              "modelId": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "peakExempt": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "visionTiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "providerId": {
                                "type": "string"
                              },
                              "modelId": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "peakExempt": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "tierBlockBaseMs": {
                          "type": "number"
                        },
                        "tierBlockMaxMs": {
                          "type": "number"
                        },
                        "proReviewConfidenceThreshold": {
                          "type": "number"
                        },
                        "peakWindows": {
                          "type": "array",
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            },
                            "minItems": 2,
                            "maxItems": 2
                          }
                        },
                        "visionWholeAnalysis": {
                          "type": "boolean"
                        },
                        "analysisMode": {
                          "type": "string",
                          "enum": [
                            "real",
                            "mock"
                          ]
                        }
                      },
                      "required": [
                        "flashTiers",
                        "proTiers",
                        "visionTiers",
                        "tierBlockBaseMs",
                        "tierBlockMaxMs",
                        "proReviewConfidenceThreshold",
                        "peakWindows",
                        "visionWholeAnalysis",
                        "analysisMode"
                      ]
                    }
                  },
                  "required": [
                    "config",
                    "model"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "issuetracker:config role required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "issueTrackerUpdateConfig",
        "summary": "Update the full tracker configuration (issuetracker:config-only)",
        "tags": [
          "Issue Tracker"
        ],
        "description": "Persists the bottom-up triage and/or model routing configuration and enqueues a `config` job to the analysis worker so it applies the change. Both sections are optional but at least one must be present and non-empty; a present section is validated (falling back to defaults on invalid fields), persisted to Mongo, and pushed to the worker. Gated on the issuetracker:config role specifically - admin does not imply this.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "config": {
                    "type": "object",
                    "description": "Bottom-up triage configuration.",
                    "required": [
                      "enabled",
                      "services",
                      "dedupTtlDays",
                      "minSeverityToOpen",
                      "autoOpen"
                    ],
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "services": {
                        "type": "object",
                        "required": [
                          "bot",
                          "api-gateway",
                          "webhooks",
                          "discord-proxy",
                          "microservices"
                        ],
                        "properties": {
                          "bot": {
                            "type": "boolean"
                          },
                          "api-gateway": {
                            "type": "boolean"
                          },
                          "webhooks": {
                            "type": "boolean"
                          },
                          "discord-proxy": {
                            "type": "boolean"
                          },
                          "microservices": {
                            "type": "boolean"
                          }
                        }
                      },
                      "dedupTtlDays": {
                        "type": "number",
                        "minimum": 1,
                        "maximum": 365
                      },
                      "minSeverityToOpen": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ]
                      },
                      "autoOpen": {
                        "type": "boolean"
                      }
                    }
                  },
                  "model": {
                    "type": "object",
                    "description": "Model routing configuration (partial - absent fields keep their stored/default value).",
                    "properties": {
                      "flashTiers": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "providerId": {
                              "type": "string"
                            },
                            "modelId": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "peakExempt": {
                              "type": "boolean"
                            }
                          }
                        }
                      },
                      "proTiers": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "providerId": {
                              "type": "string"
                            },
                            "modelId": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "peakExempt": {
                              "type": "boolean"
                            }
                          }
                        }
                      },
                      "visionTiers": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "providerId": {
                              "type": "string"
                            },
                            "modelId": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "peakExempt": {
                              "type": "boolean"
                            }
                          }
                        }
                      },
                      "tierBlockBaseMs": {
                        "type": "number",
                        "minimum": 1
                      },
                      "tierBlockMaxMs": {
                        "type": "number",
                        "minimum": 1
                      },
                      "proReviewConfidenceThreshold": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 1
                      },
                      "peakWindows": {
                        "type": "array",
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2
                        }
                      },
                      "visionWholeAnalysis": {
                        "type": "boolean"
                      },
                      "analysisMode": {
                        "type": "string",
                        "enum": [
                          "real",
                          "mock"
                        ]
                      }
                    }
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The canonical saved configuration: sections not sent in the request fall back to the stored/default values, so both keys are always present and the response is safe to store as new dashboard state.",
            "content": {
              "application/json": {
                "schema": {
                  "description": "The canonical saved configuration: sections not sent in the request fall back to the stored/default values, so both keys are always present and the response is safe to store as new dashboard state.",
                  "type": "object",
                  "properties": {
                    "config": {
                      "type": "object",
                      "description": "The bottom-up triage configuration (normalized from the request, or the loaded stored/default value when `config` was not sent).",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "services": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "boolean"
                          }
                        },
                        "dedupTtlDays": {
                          "type": "number"
                        },
                        "minSeverityToOpen": {
                          "type": "string"
                        },
                        "autoOpen": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "enabled",
                        "services",
                        "dedupTtlDays",
                        "minSeverityToOpen",
                        "autoOpen"
                      ]
                    },
                    "model": {
                      "type": "object",
                      "description": "The model routing configuration (normalized from the request, or the loaded stored/default value when `model` was not sent).",
                      "additionalProperties": true
                    }
                  },
                  "required": [
                    "config",
                    "model"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "401": {
            "description": "User is unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "issuetracker:config role required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/azurlane": {
      "post": {
        "operationId": "receiveAzurlaneWebhook",
        "summary": "Receive a Azur Lane webhook event",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives Azur Lane server status events from us-azurlane. Verified via HMAC-SHA256 of the JSON body against the Authorization header. Responds 202 immediately; the broadcast to bots is asynchronous.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/mahjongsoul": {
      "post": {
        "operationId": "receiveMahjongsoulWebhook",
        "summary": "Receive a Mahjong Soul webhook event",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives Mahjong Soul server status events from us-mahjongsoul. Verified via HMAC-SHA256 of the JSON body against the Authorization header. Responds 202 immediately; the broadcast to bots is asynchronous.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/reddit": {
      "post": {
        "operationId": "receiveRedditWebhook",
        "summary": "Receive a Reddit webhook event",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives Reddit submission events from us-reddit. Verified via HMAC-SHA256 of the JSON body against the Authorization header. Responds 202 immediately; the broadcast to bots is asynchronous.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/rss": {
      "post": {
        "operationId": "receiveRssWebhook",
        "summary": "Receive a RSS webhook event",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives RSS feed item events from us-rss. Verified via HMAC-SHA256 of the JSON body against the Authorization header. Responds 202 immediately; the broadcast to bots is asynchronous.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/topgg": {
      "post": {
        "operationId": "receiveTopggWebhook",
        "summary": "Receive a Top.gg vote webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives vote events from Top.gg, verified by plaintext token equality against the Authorization header (Top.gg does not sign payloads). Test events are acknowledged without broadcast.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/twitch": {
      "post": {
        "operationId": "receiveTwitchWebhook",
        "summary": "Receive a Twitch EventSub webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives Twitch EventSub notifications, verified via HMAC-SHA256 over messageId + timestamp + JSON body against Twitch-Eventsub-Message-Signature. Handles webhook_callback_verification challenges (text/plain echo) and notification deduplication before broadcasting.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge echo for webhook_callback_verification (text/plain)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Challenge echo for webhook_callback_verification (text/plain)",
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid EventSub headers, body or event data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Invalid verification signature or missing subscription/challenge data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/youtube": {
      "post": {
        "operationId": "receiveYoutubeWebhook",
        "summary": "Receive a YouTube PubSubHubbub notification",
        "tags": [
          "Webhooks"
        ],
        "description": "Receives YouTube Atom feed notifications (XML), verified via HMAC-SHA1 over the RAW request bytes against X-Hub-Signature. Deduplicates, enriches with channel/video data and broadcasts video_added events.",
        "responses": {
          "200": {
            "description": "Event acknowledged without processing (blacklisted channel - legacy 200 quirk)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event acknowledged without processing (blacklisted channel - legacy 200 quirk)",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Event accepted for asynchronous processing",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Event accepted for asynchronous processing",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"EVENT_RECEIVED\""
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authorization token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "youtubeChallengeResponse",
        "summary": "YouTube PubSubHubbub challenge-response",
        "tags": [
          "Webhooks"
        ],
        "description": "Echoes the hub.challenge query parameter (text/plain) for PubSubHubbub subscription verification; 403 when absent.",
        "parameters": [
          {
            "schema": "object",
            "in": "query",
            "name": "type"
          },
          {
            "schema": true,
            "in": "query",
            "name": "additionalProperties"
          }
        ],
        "responses": {
          "200": {
            "description": "Challenge echo (text/plain)",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Challenge echo (text/plain)",
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Challenge token not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/api/check": {
      "get": {
        "operationId": "apiCheck",
        "summary": "Authenticated connectivity check",
        "tags": [
          "Health"
        ],
        "description": "Returns a success message for a valid JWT or self token.",
        "responses": {
          "200": {
            "description": "Connectivity confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Connectivity confirmation",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Success message"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication token missing or revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          },
          "403": {
            "description": "Authentication token invalid or unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "healthCheck",
        "summary": "Liveness probe",
        "tags": [
          "Health"
        ],
        "description": "Returns Healthy if the gateway is alive. Used by load-balancer health checks. Unauthenticated.",
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Service is healthy",
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Always \"Healthy\""
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://gateway.cycloptux.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:3001",
      "description": "Local development"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Service health and liveness"
    },
    {
      "name": "Auth",
      "description": "Discord OAuth2 login and JWT lifecycle"
    },
    {
      "name": "Bots",
      "description": "Reverse proxy to linked bots"
    },
    {
      "name": "Services",
      "description": "API-key service endpoints"
    },
    {
      "name": "Webhooks",
      "description": "Inbound provider webhooks and fan-out"
    },
    {
      "name": "Static",
      "description": "Static data proxy"
    }
  ]
}
