{
  "openapi": "3.1.0",
  "info": {
    "title": "BrewPage API",
    "version": "1.87.0",
    "termsOfService": "https://brewpage.app/terms",
    "description": "REST API for BrewPage — HTML/KV/JSON/file hosting platform. Publish and manage web content, key-value data,\nJSON documents, and files with short URLs, password protection, and real-time statistics.\n\n## Namespaces — READ BEFORE PUBLISHING\n\n**WARNING:** If you DO NOT pass `?ns=...`, your content goes into the namespace LITERALLY NAMED `public`\n— a SHARED, WORLDWIDE-DISCOVERABLE namespace. Anything stored in `public` WITHOUT a password:\n\n- is listed on the **brewpage.app HOMEPAGE GALLERY**\n- is returned by `GET /api/gallery` (paginated, full-text search over title/tags)\n- is indexable by search engines (sitemap + meta tags)\n- can be browsed by anyone — including LLM crawlers\n\n**If you do NOT want your content on the public homepage, you MUST pass a CUSTOM NAMESPACE**\n(e.g. `?ns=myproject-2026`) and/or set a password via `X-Password`. Only then will the item\nbe excluded from `/api/gallery`. The direct short URL `/{ns}/{id}` remains reachable by anyone\nwho knows it, regardless of namespace choice.\n\n- **DEFAULT** (when `ns` is omitted): `public` ← SHARED, LISTED ON HOMEPAGE\n- **CUSTOM**: any `[a-z0-9-]{3,32}` — auto-created on first use, NOT shown in gallery\n- **Collision** on `ns+id`: server returns `409 Conflict`; retry with a different `id` or omit `id`\n\n## TTL\n\n`ttl_days` applies to html, markdown, json, kv, files, and sites. Default `15`, allowed range `1..30`\n(max `30`). Resources auto-delete after expiry.\n\n## Short URL GET resolvers\n\nEvery short-URL GET resolver (HTML, markdown, JSON, KV, file, site) returns the current view counter in the\n`X-Views: <integer>` response header.\n\n## File serving\n\nFile responses support HTTP Range requests (`Accept-Ranges: bytes`, `206 Partial Content`), carry an `ETag`\nheader, and are cached with `Cache-Control: public, max-age=3600`.\n\nAllowed file extensions include (non-exhaustive): html, md, json, txt, css, js, jsx, mjs, ts, tsx,\nplus images, audio, and video types permitted by platform limits.\n\n## Required Headers\n\n`User-Agent` is REQUIRED on every request. Format: `AgentName/version` (e.g. `Claude/4.5`, `Codex/1.0`,\n`MyBot/2.1`). Requests without User-Agent may be rate-limited or rejected. Identify yourself truthfully —\nspoofed or anonymous UAs may be flagged.\n\n## Access Logging\n\nEvery API request (both PUBLISH and READ) is persisted server-side into the `access_events` table:\nclient IP, User-Agent, HTTP method + path + query, response status, request duration (ms), UTC timestamp.\nRetention: 30 days (nightly cleanup). Admin-only endpoints: `GET /api/admin/access` (paginated log with\nfilters) and `GET /api/admin/access/stats` (aggregates). Only static assets are excluded from logging;\nevery API POST/GET/PUT/DELETE is recorded.\n",
    "contact": {
      "name": "BrewPage",
      "url": "https://brewpage.app"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    }
  },
  "externalDocs": {
    "description": "BrewPage API documentation (Scalar)",
    "url": "https://kochetkov-ma.github.io/brewpage-openapi/"
  },
  "servers": [
    {
      "url": "https://brewpage.app",
      "description": "Generated server url"
    }
  ],
  "tags": [
    {
      "name": "SEO",
      "description": "Search engine optimization endpoints"
    },
    {
      "name": "Gallery",
      "description": "Browse public content from the 'public' namespace without password protection"
    },
    {
      "name": "JSON",
      "description": "JSON document store with up to 10,000 docs per collection"
    },
    {
      "name": "Stats",
      "description": "Platform-wide usage statistics"
    },
    {
      "name": "Short Links",
      "description": "Short URL resolver for sharing"
    },
    {
      "name": "HTML",
      "description": "HTML page hosting with markdown support"
    },
    {
      "name": "KV",
      "description": "Key-Value store with up to 1000 keys per namespace"
    },
    {
      "name": "Files",
      "description": "File hosting up to 5 MB per file, 1000 files per namespace"
    },
    {
      "name": "Sites",
      "description": "Multi-file HTML site hosting via ZIP or folder upload (up to 5 MB per file, 1000 files per site)"
    },
    {
      "name": "Reports",
      "description": "Abuse reports for published content (public submission endpoint)"
    },
    {
      "name": "Admin",
      "description": "Administrative endpoints protected by the X-Admin-Password header"
    },
    {
      "name": "Namespace",
      "description": "Fresh, collision-free namespace suggestions"
    },
    {
      "name": "Owner",
      "description": "Owner token minting for claiming and updating your own resources"
    }
  ],
  "paths": {
    "/api/kv/{ns}/{id}/{key}": {
      "get": {
        "tags": [
          "KV"
        ],
        "summary": "Get key value",
        "description": "Returns the value and last update timestamp for a specific key",
        "operationId": "getKey",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param (alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key value",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvGetResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvGetResponse"
                }
              }
            }
          },
          "404": {
            "description": "Store or key not found",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvGetResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "KV"
        ],
        "summary": "Upsert key",
        "description": "Sets (creates or replaces) a key value while preserving the key short URL `/{ns}/{id}/{key}` —\nagents poll the same URL and see the latest value.\nTypical use: deploy-status counters, AI agent step state, current-config flags.\nMax 1000 keys per store. No version history: previous value is overwritten.\nRequires the owner token returned at creation.\n",
        "operationId": "upsertKey",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KvUpsertKeyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Key upserted",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvUpsertKeyResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvUpsertKeyResponse"
                }
              }
            }
          },
          "404": {
            "description": "Store not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvUpsertKeyResponse"
                }
              }
            }
          },
          "409": {
            "description": "Key limit exceeded",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvUpsertKeyResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "KV"
        ],
        "summary": "Delete key",
        "description": "Removes a single key from the store; deleting the last key does not remove the store",
        "operationId": "deleteKey",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Key deleted"
          },
          "403": {
            "description": "Missing or wrong owner token"
          },
          "404": {
            "description": "Store or key not found"
          }
        }
      }
    },
    "/api/json/{ns}/{id}": {
      "get": {
        "tags": [
          "JSON"
        ],
        "summary": "Get JSON document",
        "description": "Returns raw JSON content with application/json content type",
        "operationId": "getById",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param (alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON content",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Document not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "JSON"
        ],
        "summary": "Update JSON document",
        "description": "Replaces document content while preserving the short URL — share once, edit in place.\nUseful for AI agents iterating on a JSON artifact (test results, plan state, config snapshot)\nwithout producing a new URL each time.\nImmutable on update: tags. No version history: previous content is overwritten.\nRequires the owner token returned at creation.\n",
        "operationId": "update",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Document updated",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonUpdateResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonUpdateResponse"
                }
              }
            }
          },
          "404": {
            "description": "Document not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonUpdateResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "JSON"
        ],
        "summary": "Delete JSON document",
        "description": "Permanently removes the document",
        "operationId": "delete",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Document deleted"
          },
          "403": {
            "description": "Missing or wrong owner token"
          },
          "404": {
            "description": "Document not found or expired"
          }
        }
      }
    },
    "/api/html/{ns}/{id}": {
      "get": {
        "tags": [
          "HTML"
        ],
        "summary": "Get HTML page",
        "description": "Returns rendered HTML content; password-protected pages require X-Password header or ?p= query param",
        "operationId": "getById_1",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param (alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML content",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "HTML"
        ],
        "summary": "Update HTML page (accepts JSON or raw text/*)",
        "description": "Replaces page content while preserving the short URL — share once, edit in place.\nThe link already shared stays valid; readers see the new content on next load.\nPrimary use cases: AI agents iterating on output, fixing typos in already-shared pages.\nImmutable on update: tags, password, format, filename, showTopBar — delete and recreate to change those.\nNo version history: previous content is overwritten. Requires the owner token returned at creation.\nAlso accepts raw text/* bodies — see /llms-full.txt.\n",
        "operationId": "update_1",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Primary path: application/json with HtmlUpdateRequest. Fallback paths accept raw text/* bodies — server\nrecords the fallback admission but htmlService.update preserves the stored format. See /llms-full.txt.\n",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HtmlUpdateRequest"
              }
            },
            "text/html": {
              "schema": {
                "type": "string"
              }
            },
            "text/markdown": {
              "schema": {
                "type": "string"
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              }
            },
            "text/yaml": {
              "schema": {
                "type": "string"
              }
            },
            "text/xml": {
              "schema": {
                "type": "string"
              }
            },
            "text/csv": {
              "schema": {
                "type": "string"
              }
            },
            "text/css": {
              "schema": {
                "type": "string"
              }
            },
            "text/javascript": {
              "schema": {
                "type": "string"
              }
            },
            "text/x-toml": {
              "schema": {
                "type": "string"
              }
            },
            "application/xml": {
              "schema": {
                "type": "string"
              }
            },
            "application/yaml": {
              "schema": {
                "type": "string"
              }
            },
            "application/x-yaml": {
              "schema": {
                "type": "string"
              }
            },
            "application/javascript": {
              "schema": {
                "type": "string"
              }
            },
            "application/toml": {
              "schema": {
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Page updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlUpdateResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "HTML"
        ],
        "summary": "Delete HTML page",
        "description": "Permanently removes page and frees the short URL",
        "operationId": "delete_1",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Page deleted"
          },
          "403": {
            "description": "Missing or wrong owner token"
          },
          "404": {
            "description": "Page not found or expired"
          }
        }
      }
    },
    "/api/html/{ns}/{id}/settings": {
      "get": {
        "tags": [
          "HTML"
        ],
        "summary": "Get HTML page settings",
        "description": "Returns the republish-prefill projection (namespace, ttlDays, hasPassword, showTopBar,\nformat, title, filename, createdAt, expiresAt) without the content body. Owner-only —\nrequires the X-Owner-Token returned at creation. Designed for the SPA Republish entry\nfrom gallery cards and the in-page badge: callers supplying a fresh body skip the cost\nof shipping the previous content (HTML rows can be up to 5 MB). Mirrors `/source`'s\n404/403 semantics; a blank token returns 400 with `OWNER_TOKEN_REQUIRED` so the SPA\ncan distinguish \"no token pasted yet\" from \"token rejected\".\n",
        "operationId": "getHtmlSettings",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for settings access",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settings projection",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlSettingsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Owner token missing (code OWNER_TOKEN_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/html/{ns}/{id}/source": {
      "get": {
        "tags": [
          "HTML"
        ],
        "summary": "Get HTML page raw source",
        "description": "Returns the raw user-submitted body (pre-render, pre-sanitize) plus metadata for\nrepublish (format, title, namespace, id, ttlDays, createdAt, expiresAt, filename,\nshowTopBar). Owner-only — requires the X-Owner-Token returned at creation. The\nserved bytes are the verbatim original — markdown is NOT re-rendered, HTML is NOT\nre-sanitized. A blank token returns 400 with `OWNER_TOKEN_REQUIRED` so the SPA can\ndistinguish \"no token pasted yet\" from \"token rejected\".\n",
        "operationId": "getHtmlSource",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for source access",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw source body and metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlSourceResponse"
                }
              }
            }
          },
          "400": {
            "description": "Owner token missing (code OWNER_TOKEN_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/kv": {
      "get": {
        "tags": [
          "KV"
        ],
        "summary": "List KV stores",
        "description": "Returns all KV stores owned by the given token in the namespace. Returns empty list without token",
        "operationId": "listStores",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token to filter by ownership. Without token returns empty list",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Store list",
            "content": {
              "*/*": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/KvStoreListResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "KV"
        ],
        "summary": "Create KV store",
        "description": "Creates a new store with an initial key-value pair and returns a shareable link. Reuse existing owner token to group entities under one owner",
        "operationId": "createStore",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace. Default: public. All namespaces are PUBLIC and indexable. Pages in 'public' without password appear in gallery. Custom namespace is created automatically. Format: [a-z0-9-]{3,32}. On ns+id collision the server returns 409 Conflict — retry with a different id or omit id to let the server generate one",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public",
              "pattern": "^[a-z0-9-]{3,32}$"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). Store auto-deletes after expiry",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password. Empty = public store visible in gallery. With password = hidden from gallery, viewers must enter password or pass ?p= in URL",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Reuse existing owner token to group entities under one owner. If omitted, a new token is generated",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KvCreateStoreRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Store created",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvCreateStoreResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request parameters",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvCreateStoreResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvCreateStoreResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/json": {
      "get": {
        "tags": [
          "JSON"
        ],
        "summary": "List JSON documents",
        "description": "Returns documents owned by the given token. Empty list without token",
        "operationId": "list",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token to filter by ownership. Without token returns empty list",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Document list",
            "content": {
              "*/*": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/JsonListResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "JSON"
        ],
        "summary": "Create JSON document",
        "description": "Stores any valid JSON and returns a shareable link. Reuse existing owner token to group entities under one owner",
        "operationId": "create",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace. Default: public. All namespaces are PUBLIC and indexable. Pages in 'public' without password appear in gallery. Custom namespace is created automatically. Format: [a-z0-9-]{3,32}. On ns+id collision the server returns 409 Conflict — retry with a different id or omit id to let the server generate one",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public",
              "pattern": "^[a-z0-9-]{3,32}$"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). Document auto-deletes after expiry.\nSince v1.49.0 the query string also accepts a suffixed form like `'30d'` or `'30 days'`\n(parsed server-side back to an integer) — useful for clients that prefer human-readable\ndurations.\n",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password. Empty = public document visible in gallery. With password = hidden from gallery, viewers must enter password or pass ?p= in URL",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Reuse existing owner token to group entities under one owner. If omitted, a new token is generated",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Document created",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonCreateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON or request parameters",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonCreateResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/JsonCreateResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/html": {
      "post": {
        "tags": [
          "HTML"
        ],
        "summary": "Create HTML page (accepts JSON or raw text/*/octet-stream)",
        "description": "Stores HTML or markdown content and returns a shareable link. Reuse\nexisting owner token to group entities under one owner. Accepts three\ncontent-type families: `application/json` (structured `HtmlUploadRequest`\npayload — primary path), raw `text/*` (HTML, Markdown, YAML, XML, CSV,\ncode — server detects format), and `application/octet-stream` (binary —\nauto-routes to /api/files for png/jpg/gif/webp/mp4/pdf/zip; otherwise\ntext-sniffed). See /llms-full.txt.\n\nIdempotent for the (owner_token, content, public/, 24h) tuple:\na byte-identical repost from the same owner to `ns=public` without a\npassword within 24h returns the existing id. Response status stays\n201 and body shape is unchanged; the silently-merged response carries\nheader `X-Existing-Resource: 1`. To replace content at the same id\nuse `PUT /api/html/{ns}/{id}` (the canonical republish path).\n",
        "operationId": "create_1",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace. Default: public. All namespaces are PUBLIC and indexable. Pages in 'public' without password appear in gallery. Custom namespace is created automatically. Format: [a-z0-9-]{3,32}. On ns+id collision the server returns 409 Conflict — retry with a different id or omit id to let the server generate one",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public",
              "pattern": "^[a-z0-9-]{3,32}$"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). Page auto-deletes after expiry.\nSince v1.49.0 the query string also accepts a suffixed form like `'30d'` or `'30 days'`\n(parsed server-side back to an integer) — useful for clients that prefer human-readable\ndurations.\n",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Content format: 'html' (default), 'markdown', or 'md'. Markdown is rendered to styled HTML with github-markdown-css",
            "required": false,
            "schema": {
              "type": "string",
              "default": "html"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password. Empty = public page visible in gallery. With password = hidden from gallery, viewers must enter password or pass ?p= in URL",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Reuse existing owner token to group entities under one owner. If omitted, a new token is generated",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Primary path: `application/json` with `HtmlUploadRequest`. Fallback paths accept raw text/* and\n`application/octet-stream` bodies — server detects format (or use `?format=`). See `/llms-full.txt`.\n",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HtmlUploadRequest"
              }
            },
            "text/html": {
              "schema": {
                "type": "string"
              }
            },
            "text/markdown": {
              "schema": {
                "type": "string"
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              }
            },
            "text/yaml": {
              "schema": {
                "type": "string"
              }
            },
            "text/xml": {
              "schema": {
                "type": "string"
              }
            },
            "text/csv": {
              "schema": {
                "type": "string"
              }
            },
            "text/css": {
              "schema": {
                "type": "string"
              }
            },
            "text/javascript": {
              "schema": {
                "type": "string"
              }
            },
            "text/x-toml": {
              "schema": {
                "type": "string"
              }
            },
            "application/xml": {
              "schema": {
                "type": "string"
              }
            },
            "application/yaml": {
              "schema": {
                "type": "string"
              }
            },
            "application/x-yaml": {
              "schema": {
                "type": "string"
              }
            },
            "application/javascript": {
              "schema": {
                "type": "string"
              }
            },
            "application/toml": {
              "schema": {
                "type": "string"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "Binary — auto-routes to /api/files for png/jpg/gif/webp/mp4/pdf/zip; otherwise text-sniffed."
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Page created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlUploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported Content-Type — body did not match any supported text/* or application/* fallback.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Body parsed but failed validation (e.g. binary detected on text path, size limit exceeded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/files": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "List files",
        "description": "Returns files owned by the given token. Empty list without token",
        "operationId": "list_1",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token to filter by ownership. Without token returns empty list",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File list",
            "content": {
              "*/*": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FileListResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Files"
        ],
        "summary": "Upload file",
        "description": "Stores a file via multipart upload (part name `file`) and returns a download link. Only safe file types\naccepted. Size caps: 5 MB generic, video up to 20 MB, audio up to 5 MB. Up to 1000 files per namespace.\nReuse existing owner token to group entities under one owner.\n",
        "operationId": "upload",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace. Default: public. All namespaces are PUBLIC and indexable. Pages in 'public' without password appear in gallery. Custom namespace is created automatically. Format: [a-z0-9-]{3,32}. On ns+id collision the server returns 409 Conflict — retry with a different id or omit id to let the server generate one",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public",
              "pattern": "^[a-z0-9-]{3,32}$"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). File auto-deletes after expiry.\nSince v1.49.0 the query string also accepts a suffixed form like `'30d'` or `'30 days'`\n(parsed server-side back to an integer) — useful for clients that prefer human-readable\ndurations.\n",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password. Empty = public file visible in gallery. With password = hidden from gallery, viewers must enter password or pass ?p= in URL",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Reuse existing owner token to group entities under one owner. If omitted, a new token is generated",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to upload. The original filename is preserved from the multipart part headers."
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileUploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or file too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported file type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "File parsed but failed validation (e.g. extension allowed but content sniffed as a disallowed type).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/{ns}/{id}": {
      "get": {
        "tags": [
          "Short Links"
        ],
        "summary": "Resolve short URL",
        "description": "Resolves a short URL to the underlying resource (HTML, markdown, JSON, KV, file, or site entry).\n\nThe `?raw=1` query parameter bypasses the SPA shell and streams raw resource bytes directly.\nRequired for browser-native consumers that cannot run the SPA wrapper — `<video src>`, `<audio src>`,\n`<iframe src>` for PDF, and any other embed that must receive the original Content-Type rather\nthan the SPA's HTML wrapper.\n\nResponse headers:\n- `X-Views: <integer>` — current view counter for this resource.\n- For file/site responses: `Accept-Ranges: bytes`, `ETag`, and `Cache-Control: public, max-age=3600`.\n  Range requests return `206 Partial Content`.\n- For HTML resolves: `X-Show-Top-Bar: true|false` — effective resolved value of `showTopBar`\n  (per-page override, else server default `app.ui.show-top-bar-default`).\n",
        "operationId": "resolve",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password for protected resources",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password (query alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dl",
            "in": "query",
            "description": "Force download as attachment",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "raw",
            "in": "query",
            "description": "Bypass the SPA wrapper and stream raw resource bytes directly with the original Content-Type.\nRequired for `<video src>`, `<audio src>`, `<iframe src>` for PDF, and other browser-native embeds\nthat cannot run the SPA shell.\n",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Views": {
                "description": "Current view counter for the resolved resource",
                "schema": {
                  "type": "integer",
                  "format": "int64"
                }
              },
              "Accept-Ranges": {
                "description": "Present for file/site responses: bytes",
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "description": "Entity tag for file/site responses",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Present for file/site responses: public, max-age=3600",
                "schema": {
                  "type": "string"
                }
              },
              "X-Show-Top-Bar": {
                "description": "Effective resolved value of showTopBar for HTML content (per-page override,\nelse server default app.ui.show-top-bar-default). Reflects whether the served\npage renders the BrewPage top toolbar. Present on HTML resolves only.\n",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "206": {
            "description": "Partial Content (Range requests for files/sites)",
            "headers": {
              "Content-Range": {
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or expired (not yet cleaned up)",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflict — ns+id already exists. Retry with a different id or omit id to let the server generate one"
          },
          "410": {
            "description": "Gone — the page existed and is now permanently removed (deleted by its owner or\nexpired and cleaned up). Returned for `public/` namespace resources only, so that\nsearch engines de-index them faster than a 404. Non-public namespaces always return\n404 to avoid a private-existence oracle.\n",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/{ns}/{id}/{sub}": {
      "get": {
        "tags": [
          "Short Links"
        ],
        "summary": "Resolve short URL with sub-path",
        "description": "Resolves a short URL with a sub-path (e.g. entry file within an uploaded site).\n\nResponse headers:\n- `X-Views: <integer>` — current view counter for this resource.\n- For file/site responses: `Accept-Ranges: bytes`, `ETag`, and `Cache-Control: public, max-age=3600`.\n  Range requests return `206 Partial Content`.\n",
        "operationId": "resolveWithSub",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password for protected resources",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password (query alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dl",
            "in": "query",
            "description": "Force download as attachment",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Views": {
                "description": "Current view counter for the resolved resource",
                "schema": {
                  "type": "integer",
                  "format": "int64"
                }
              },
              "Accept-Ranges": {
                "description": "Present for file/site responses: bytes",
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "description": "Entity tag for file/site responses",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Present for file/site responses: public, max-age=3600",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "206": {
            "description": "Partial Content (Range requests for files/sites)",
            "headers": {
              "Content-Range": {
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Page or sub-path not found or expired (not yet cleaned up)",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "410": {
            "description": "Gone — the page existed and is now permanently removed (deleted by its owner or\nexpired and cleaned up). Returned for `public/` namespace resources only, so that\nsearch engines de-index them faster than a 404. Non-public namespaces always return\n404 to avoid a private-existence oracle.\n",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/{key}.txt": {
      "get": {
        "tags": [
          "SEO"
        ],
        "summary": "IndexNow key verification",
        "description": "Serves the IndexNow verification key file for search engine crawlers",
        "operationId": "serveKeyFile",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key file content",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown key",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get platform stats",
        "description": "Returns today's and all-time creation/view counts with per-type breakdown.\n\nDisjoint split semantics: `*Public` and `*Private` count alive (non-deleted) resources only;\n`*Deleted` is a separate disjoint bucket. Invariant: `Public + Private + Deleted == Total`.\n\nOptional `tz` query param shifts the boundary of \"today\" to the given IANA timezone.\nDefaults to UTC. Invalid value falls back to UTC silently.\n",
        "operationId": "getStats",
        "parameters": [
          {
            "name": "tz",
            "in": "query",
            "description": "IANA timezone id for the \"today\" boundary (e.g. Europe/Lisbon, Asia/Tokyo). Defaults to UTC.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Platform statistics",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/StatsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/sitemap.xml": {
      "get": {
        "tags": [
          "SEO"
        ],
        "summary": "Dynamic XML sitemap",
        "description": "Generates sitemap with static pages and all public gallery entries. Caddy should route /sitemap.xml to this endpoint",
        "operationId": "sitemap",
        "responses": {
          "200": {
            "description": "Sitemap XML",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/kv/{ns}/{id}": {
      "get": {
        "tags": [
          "KV"
        ],
        "summary": "List keys",
        "description": "Returns all key names and total count for a store",
        "operationId": "listKeys",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param (alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key list",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvListKeysResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvListKeysResponse"
                }
              }
            }
          },
          "404": {
            "description": "Store not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/KvListKeysResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "KV"
        ],
        "summary": "Delete KV store",
        "description": "Deletes the entire KV store and all its keys. Requires owner token.",
        "operationId": "deleteKvStore",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token required to delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Store deleted"
          },
          "403": {
            "description": "Invalid owner token"
          },
          "404": {
            "description": "Store not found or expired"
          }
        }
      }
    },
    "/api/gallery": {
      "get": {
        "tags": [
          "Gallery"
        ],
        "summary": "Browse gallery",
        "description": "Lists public pages (public namespace, no password) with optional case-insensitive search by title/tags",
        "operationId": "getGallery",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive search by title or tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-based)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Items per page (max 100)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 20
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort order for gallery results. `date` (default) — newest first by creation date. `views` — most viewed first, then newest by date as tiebreaker.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "date",
                "views"
              ],
              "default": "date"
            }
          },
          {
            "name": "mine",
            "in": "query",
            "description": "When true, filter to only the caller's own resources; requires X-Owner-Token header.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Required only when mine=true; otherwise ignored.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated gallery items",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/GalleryResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/files/{ns}/{id}": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "Download file",
        "description": "Returns file inline for previewable types (images, PDF, media) or as attachment. Add ?dl=1 to force download.",
        "operationId": "download",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param (alternative to X-Password header)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dl",
            "in": "query",
            "description": "Force download as attachment",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File content",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "byte"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "byte"
                }
              }
            }
          },
          "404": {
            "description": "File not found or expired",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "byte"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Files"
        ],
        "summary": "Delete file",
        "description": "Permanently removes the file from storage",
        "operationId": "delete_2",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for update and delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "File deleted"
          },
          "403": {
            "description": "Missing or wrong owner token"
          },
          "404": {
            "description": "File not found or expired"
          }
        }
      }
    },
    "/api/sites": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Upload site",
        "description": "Upload a multi-file HTML site as a ZIP archive or as individual files with paths. Returns a link to the published site",
        "operationId": "uploadSite",
        "parameters": [
          {
            "name": "ns",
            "in": "query",
            "description": "Namespace. Default: public",
            "required": false,
            "schema": {
              "type": "string",
              "default": "public"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). Site auto-deletes after expiry",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "entry",
            "in": "query",
            "description": "Entry file path override (default: auto-detect index.html)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Reuse existing owner token to group entities under one owner",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "archive": {
                    "type": "string",
                    "format": "binary",
                    "description": "ZIP archive containing site files"
                  },
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Individual files (alternative to archive)"
                  },
                  "paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Relative paths for each file (must match files order)"
                  }
                }
              },
              "examples": {
                "archive_zip": {
                  "summary": "Upload a ZIP archive (archive mode)",
                  "description": "Send the ZIP as `archive` field. Query string controls namespace, TTL, and entry file.\ncurl equivalent:\n  curl -X POST \"https://brewpage.app/api/sites?ns=docs&ttl=15&entry=index.html\" \\\n    -F \"archive=@site.zip\"\n",
                  "value": {
                    "archive": "<binary zip content>"
                  }
                },
                "files_paths": {
                  "summary": "Upload individual files with explicit paths (parallel-arrays mode)",
                  "description": "Send each file as a `files` field and its relative path as the matching `paths` field.\nThe arrays must be the same length and in the same order.\ncurl equivalent:\n  curl -X POST \"https://brewpage.app/api/sites?ns=docs&ttl=15&entry=index.html\" \\\n    -F \"files=@index.html\" -F \"paths=index.html\" \\\n    -F \"files=@about.html\" -F \"paths=about.html\" \\\n    -F \"files=@gallery.html\" -F \"paths=gallery.html\" \\\n    -F \"files=@contact.html\" -F \"paths=contact.html\"\n",
                  "value": {
                    "files": [
                      "<binary content of index.html>",
                      "<binary content of about.html>",
                      "<binary content of gallery.html>",
                      "<binary content of contact.html>"
                    ],
                    "paths": [
                      "index.html",
                      "about.html",
                      "gallery.html",
                      "contact.html"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteUploadResponse"
                },
                "examples": {
                  "site_created": {
                    "summary": "Successful site upload response",
                    "value": {
                      "id": "a1b2c3d4e5",
                      "namespace": "docs",
                      "entryFile": "index.html",
                      "link": "https://brewpage.app/docs/a1b2c3d4e5",
                      "ownerLink": "https://brewpage.app/api/sites/docs/a1b2c3d4e5",
                      "fileCount": 4,
                      "totalSizeBytes": 24576,
                      "expiresAt": "2026-05-16T00:00:00Z",
                      "tags": [],
                      "ownerToken": "9f3a1c7d2e4b8a056f1234567890abcd"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, no HTML files, or limits exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported file type in archive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Archive parsed but failed validation (e.g. file extension allowed but content sniffed as a disallowed type).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites/{ns}/{id}": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Get site info",
        "description": "Returns site metadata and file list. Requires owner token",
        "operationId": "getSiteInfo",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Site info",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteInfoResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Site not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Sites"
        ],
        "summary": "Republish site (full replace)",
        "description": "Republish a site in place (full replace) at the same URL. The uploaded bundle becomes the complete\nnew file set — files absent from it are removed, new files added, matching files overwritten.\nRequires the owner token.\n\nThe owner token is REQUIRED: a missing `X-Owner-Token` header returns 403 (same as a wrong token).\nUpload modes mirror `POST /api/sites`: send a ZIP `archive`, or parallel `files[]` + `paths[]` arrays.\n",
        "operationId": "republishSite",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags (replaces existing tags)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "description": "Time to live in days (1-30, default 15, max 30). Site auto-deletes after expiry",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 15
            }
          },
          {
            "name": "entry",
            "in": "query",
            "description": "Entry file path override (default: auto-detect index.html)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for republish",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "archive": {
                    "type": "string",
                    "format": "binary",
                    "description": "ZIP archive containing the complete new site file set"
                  },
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Individual files (alternative to archive)"
                  },
                  "paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Relative paths for each file (must match files order)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Site republished (full replace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteUpdateResponse"
                },
                "examples": {
                  "site_updated": {
                    "summary": "Successful site republish response",
                    "value": {
                      "id": "a1b2c3d4e5",
                      "namespace": "docs",
                      "entryFile": "index.html",
                      "link": "https://brewpage.app/docs/a1b2c3d4e5",
                      "ownerLink": "https://brewpage.app/api/sites/docs/a1b2c3d4e5",
                      "fileCount": 5,
                      "totalSizeBytes": 30720,
                      "expiresAt": "2026-06-23T00:00:00Z",
                      "tags": [],
                      "result": {
                        "code": "success",
                        "action": "updated"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty bundle, no HTML files, limits exceeded, or invalid ttl",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Site not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Site is gone (soft-hidden)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported file type in archive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Delete site",
        "description": "Permanently removes the site and all its files from storage",
        "operationId": "deleteSite",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token returned at creation. Required for delete",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Site deleted"
          },
          "403": {
            "description": "Missing or wrong owner token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Site not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites/{ns}/{id}/files/{filePath}": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Serve site file",
        "description": "Serves an individual file from the site with correct content type",
        "operationId": "serveSiteFile",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filePath",
            "in": "path",
            "required": true,
            "description": "Relative file path within the site. May contain slashes for nested paths, e.g.: assets/css/main.css",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Password",
            "in": "header",
            "description": "Access password via header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "p",
            "in": "query",
            "description": "Access password via query param",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File content",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "Wrong password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Site or file not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/reports": {
      "post": {
        "tags": [
          "Reports"
        ],
        "summary": "Submit abuse report",
        "description": "Submit a report about content hosted on brewpage.app or brewdata.app. Public endpoint, rate-limited (60/hr/IP). The server records the reporter's IP (from X-Forwarded-For) and User-Agent automatically",
        "operationId": "submitReport",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Report accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failure (invalid URL, unknown category, description out of range, malformed email)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/reports": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List abuse reports",
        "description": "List abuse reports with optional status filtering and pagination. Requires admin password",
        "operationId": "listReports",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by report status",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "new",
                "reviewed",
                "dismissed",
                "actioned"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of reports to return",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Offset into the result set for pagination",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reports list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReportSummary"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/reports/{id}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Get abuse report",
        "description": "Fetch the full details of a single abuse report, including reporter metadata and reviewer note. Requires admin password",
        "operationId": "getReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "10-character alphanumeric report ID",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{10}$"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Report detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportDetail"
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Report not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Admin"
        ],
        "summary": "Update abuse report",
        "description": "Update the status and reviewer note of a report. Requires admin password",
        "operationId": "updateReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "10-character alphanumeric report ID",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{10}$"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportUpdateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Updated report detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportDetail"
                }
              }
            }
          },
          "400": {
            "description": "Invalid status value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Report not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/access": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List access log events",
        "description": "List recent HTTP access events persisted into `access_events`. Every external request is logged with\nIP, User-Agent, method, path, status, and latency. Retention is 30 days (nightly cleanup job).\nRequires admin password.\n\nFilters are mutually exclusive and applied in priority order: `resource` > `ip` > (none, returns most recent).\n",
        "operationId": "listAccess",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "resource",
            "in": "query",
            "description": "Filter events by resource ID (e.g. a 10-character short ID). Takes priority over `ip`",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ip",
            "in": "query",
            "description": "Filter events by client IP address. Ignored if `resource` is also provided",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of events to return",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Offset into the result set for pagination",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Access events list, most recent first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AccessEventSummary"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/access/stats": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Aggregated access log stats",
        "description": "Aggregated statistics over the access log for the last `days` days, including totals, unique IPs,\ntop paths / IPs / user-agents / referers, and a request-count distribution over the 24 hours of day.\nRequires admin password.\n",
        "operationId": "accessStats",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "description": "Number of days to look back from now",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregated access stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessStats"
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/owners": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List or search owners",
        "description": "Lists every owner token (hashed identifier) along with per-resource counts (HTML, KV stores, JSON\ndocuments, files, sites), the first-seen timestamp, and the owner's tier/label. Metadata only — no\nstored content is returned. Requires admin password.\n\nOptional `search` narrows the result: a 32-character base62 value is treated as a raw owner token and\nresolved to its `owner_id`; any other value is matched as an `owner_id` prefix.\n",
        "operationId": "listOwners",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Optional owner filter. A 32-character base62 string is resolved as a raw owner token to its\nowner_id; any other value is matched as an owner_id prefix.\n",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Owners list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OwnerInfo"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/owners/resolve": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Resolve owner token to owner ID",
        "description": "Resolves a raw owner token to its hashed `ownerId`. Lets an operator look up the owner record that\nbacks a token shared by a user (e.g. for a tier-promotion request) without exposing the hashing.\nMetadata only. Requires admin password.\n",
        "operationId": "resolveOwner",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "token",
            "in": "query",
            "description": "Raw owner token to resolve",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved owner ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerResolveResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header or missing token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/owners/{ownerId}/promote": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Promote owner tier",
        "description": "Sets an owner's tier. `PRIVILEGED` raises the owner's maximum TTL ceiling (up to 730 days) and may\nattach an operator label/note; `DEFAULT` keeps standard limits. Metadata only. Requires admin password.\n",
        "operationId": "promoteOwner",
        "parameters": [
          {
            "name": "ownerId",
            "in": "path",
            "description": "Hashed owner identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnerPromoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Owner tier updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerTierResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header, invalid tier, or maxTtlDays out of range (PRIVILEGED ceiling 730)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Owner not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/owners/{ownerId}/demote": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Demote owner to default tier",
        "description": "Reverts an owner to the `DEFAULT` tier and standard limits. Idempotent — demoting an already-default\nowner succeeds with no change. Metadata only. Requires admin password.\n",
        "operationId": "demoteOwner",
        "parameters": [
          {
            "name": "ownerId",
            "in": "path",
            "description": "Hashed owner identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Owner reverted to DEFAULT tier (or already default)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerTierResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/owners/{ownerId}": {
      "delete": {
        "tags": [
          "Admin"
        ],
        "summary": "Delete owner",
        "description": "Deletes an owner and all resources created under that owner token. Requires admin password.\n",
        "operationId": "deleteOwner",
        "parameters": [
          {
            "name": "ownerId",
            "in": "path",
            "description": "Hashed owner identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Owner deleted"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Owner not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/stats": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Platform admin stats",
        "description": "Aggregate platform counts (total HTML pages, KV entries, JSON documents, files, sites, unique owners)\nplus a per-owner breakdown. Metadata only. Requires admin password.\n",
        "operationId": "adminStats",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregate admin stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminStatsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/blocklist": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List blocklist entries",
        "description": "Lists IP / User-Agent blocklist entries (abuse mitigation). Metadata only — IPs, UA patterns, reason,\ntimestamps, and active flag. Requires admin password.\n",
        "operationId": "listBlocklist",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active",
            "in": "query",
            "description": "Filter by active flag. When omitted, defaults to active-only unless `includeInactive=true`",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by entry type",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeInactive",
            "in": "query",
            "description": "Include expired/inactive entries when no explicit `active` filter is given",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of entries to return (clamped to 1..1000)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Offset into the result set for pagination",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blocklist entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BlocklistEntryResponse"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Add blocklist entry",
        "description": "Adds an IP and/or User-Agent pattern to the abuse blocklist with an optional ISO-8601 duration\n(e.g. `PT1H`, `P1D`). At least one of `ip` or `uaPattern` is required. Requires admin password.\n",
        "operationId": "addBlocklistEntry",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlocklistEntryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Blocklist entry created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlocklistEntryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/blocklist/{id}": {
      "delete": {
        "tags": [
          "Admin"
        ],
        "summary": "Remove blocklist entry",
        "description": "Removes a blocklist entry by numeric ID. Requires admin password.",
        "operationId": "deleteBlocklistEntry",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric blocklist entry ID",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Entry removed"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Entry not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/llm-config": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Get content-protection configuration",
        "description": "Returns the current content-protection (automated scanner) configuration as a masked snapshot.\nSensitive credentials are never returned in clear text. Response shape is intentionally\nunspecified here as it covers internal protection settings. Requires admin password.\n",
        "operationId": "getLlmConfig",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Masked configuration snapshot"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Admin"
        ],
        "summary": "Update content-protection configuration",
        "description": "Applies a partial update to the content-protection configuration and returns the new masked\nsnapshot. Outbound endpoint and timeout are configured via deployment environment only and\ncannot be changed through this endpoint. Requires admin password.\n",
        "operationId": "updateLlmConfig",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Partial configuration patch. Internal — shape omitted from the public contract.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated masked configuration snapshot"
          },
          "400": {
            "description": "Empty patch or missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/llm-config/rollback-prompt": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Roll back content-protection prompt",
        "description": "Restores the previously stored content-protection prompt and returns the new masked snapshot.\nReturns 409 when there is no previous prompt to restore. Requires admin password.\n",
        "operationId": "rollbackLlmConfigPrompt",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated masked configuration snapshot"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No previous prompt to roll back to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/publications": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List publications (admin)",
        "description": "Paged administrative listing of published resources with their review state. Metadata only — type,\nnamespace, ID, timestamps, and review state; no stored content is returned. Internal review fields\nare omitted from the public contract. Requires admin password.\n",
        "operationId": "listPublications",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by resource type",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "namespace",
            "in": "query",
            "description": "Filter by exact namespace",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "query",
            "description": "Filter by resource ID",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (clamped to 1..200)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Paging offset (clamped to 0..10000)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged publications list"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/publications/rescan": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Re-queue publications for review",
        "description": "Re-queues a batch of publications for automated content review. Request/response shapes cover internal\nreview queueing and are omitted from the public contract. Requires admin password.\n",
        "operationId": "rescanPublications",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Batch of items to re-queue. Internal — shape omitted from the public contract.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item re-queue outcomes"
          },
          "400": {
            "description": "Batch out of range or missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Automated review currently unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/publications/queue-depth": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Review queue depth",
        "description": "Observability hook returning the current review queue size and capacity. Requires admin password.\n",
        "operationId": "publicationsQueueDepth",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Queue size and capacity"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/scan-events": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List content-review events",
        "description": "Lists recent automated content-review events (metadata only). Response shape covers internal review\ndetails and is omitted from the public contract. Requires admin password.\n",
        "operationId": "listScanEvents",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of events to return (clamped to 1..200)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent content-review events"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/spam-events": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "List abuse-mitigation events",
        "description": "Lists recent abuse-mitigation events (metadata only — namespace, related IDs, IP, User-Agent,\ntimestamps). Response shape covers internal mitigation details and is omitted from the public\ncontract. Requires admin password.\n",
        "operationId": "listSpamEvents",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Filter by event kind",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "description": "Number of days to look back (clamped to 1..365)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 30
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of events to return (clamped to 1..500)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Offset into the result set for pagination",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent abuse-mitigation events"
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/spam-sweep/preview": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Preview gallery abuse sweep",
        "description": "Read-only dry-run preview of the public-gallery abuse-protection sweep. Operates on the public\nnamespace only. Internal preview details are omitted from the public contract. Requires admin password.\n",
        "operationId": "spamSweepPreview",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lookbackHours",
            "in": "query",
            "description": "Look-back window in hours (defaults to the configured value when omitted)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dry-run preview"
          },
          "400": {
            "description": "Invalid look-back value or missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/spam-sweep/run": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Run gallery abuse sweep",
        "description": "Runs the public-gallery abuse-protection sweep synchronously and returns a run summary. Operates on\nthe public namespace only. Internal run details are omitted from the public contract. Requires admin\npassword.\n",
        "operationId": "spamSweepRun",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lookbackHours",
            "in": "query",
            "description": "Look-back window in hours (defaults to the configured value when omitted)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "force",
            "in": "query",
            "description": "Force re-evaluation regardless of prior runs",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Run summary"
          },
          "400": {
            "description": "Invalid look-back value or missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/ttl/auto-extend/preview": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Preview TTL auto-extend",
        "description": "Read-only dry-run preview of the TTL auto-extend job: returns per-resource candidate counts and the\neffective threshold for the public namespace. Metadata only. Requires admin password.\n",
        "operationId": "ttlAutoExtendPreview",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "TTL auto-extend preview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoExtendPreviewResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/ttl/auto-extend/run": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Run TTL auto-extend",
        "description": "Runs the TTL auto-extend job synchronously and returns per-resource extended counts and run duration.\nRequires admin password.\n",
        "operationId": "ttlAutoExtendRun",
        "parameters": [
          {
            "name": "X-Admin-Password",
            "in": "header",
            "description": "Admin password",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "TTL auto-extend run summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoExtendRunResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing admin password header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Wrong admin password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/{ns}/{id}/owner-check": {
      "get": {
        "tags": [
          "Owner Check"
        ],
        "summary": "Verify whether the supplied X-Owner-Token owns the resource",
        "description": "Returns `{isOwner, type}`. Constant-time BCrypt match.\n404 when the resource does not exist (or has expired).\n200 with `isOwner:false` when the X-Owner-Token header is absent.\n",
        "operationId": "ownerCheck",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Owner-Token",
            "in": "header",
            "description": "Owner token to verify (constant-time match)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerCheckResponse"
                }
              }
            }
          },
          "404": {
            "description": "Resource does not exist or has expired"
          }
        }
      }
    },
    "/preview-html/{ns}/{id}": {
      "get": {
        "tags": [
          "preview"
        ],
        "summary": "OpenGraph HTML stub",
        "description": "Tiny HTML response with og:title/og:description/og:image meta tags for social-bot unfurls",
        "operationId": "previewHtml",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML stub with OG meta (or generic stub when resource is missing)",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/preview/{ns}/{id}.png": {
      "get": {
        "tags": [
          "preview"
        ],
        "summary": "Per-content OG image",
        "description": "Returns 1200×630 PNG, cached, with ETag/If-None-Match support; falls back to /og-image.png?v=2 on any failure",
        "operationId": "preview",
        "parameters": [
          {
            "name": "ns",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image bytes",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Fallback to static og-image.png (flag off, no source, generation error, oversized)"
          },
          "304": {
            "description": "Not modified (If-None-Match matched current ETag)"
          },
          "429": {
            "description": "Per-IP rate limit exceeded"
          }
        }
      }
    },
    "/api/owner-token": {
      "get": {
        "tags": [
          "Owner"
        ],
        "summary": "Generate owner token",
        "description": "Mints a fresh owner token. Free, no authentication required. This endpoint uses a dedicated\nper-IP token-generation rate limit (default 60/min/IP), separate from the shared read budget.\n\nSend the returned `token` in the `X-Owner-Token` header on subsequent calls to claim and own the\nresources you create. The token is REQUIRED to update your own content: `PUT` updates to a site\n(and other owner-gated mutations) demand a matching `X-Owner-Token` header.\n\nThe companion `ownerId` is the SHA-256 hash (64-char lowercase hex) of the token — the stable,\nnon-secret identifier the platform stores against your resources. Keep the `token` itself secret;\nanyone holding it can modify resources you own. A lost token cannot be recovered — mint a new one.\n",
        "operationId": "generateOwnerToken",
        "responses": {
          "200": {
            "description": "Owner token generated",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerTokenResponse"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP rate limit exceeded"
          }
        }
      }
    },
    "/api/namespace/random": {
      "get": {
        "tags": [
          "Namespace"
        ],
        "summary": "Suggest a random namespace",
        "description": "Returns a fresh `<word>-<word>-NN` namespace from the EFF Short Wordlist that does not collide\nwith any existing resource (html, kv, json, file, site). Pure read; no resource is allocated.\n\nUse this to suggest a unique namespace to the user before publishing — pass the returned value\nas the `?ns=` query parameter on subsequent publish calls. Subject to the same per-IP read\nrate limit as other GET endpoints (default 300/min/IP).\n",
        "operationId": "randomNamespace",
        "responses": {
          "200": {
            "description": "Namespace suggested",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/RandomNamespaceResponse"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP rate limit exceeded"
          },
          "503": {
            "description": "Namespace pool exhausted — retry budget hit consecutive collisions"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "OwnerCheckResponse": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Resource type (html, kv, json, file, site)"
          },
          "isOwner": {
            "type": "boolean",
            "description": "True when the supplied owner token matches the stored hash"
          }
        }
      },
      "OwnerTokenResponse": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "Fresh owner token (32-char base62). Send it as the `X-Owner-Token` header to claim and update\nyour own resources. Keep it secret; required for PUT site updates.\n",
            "pattern": "^[A-Za-z0-9]{32}$",
            "example": "7Qm2KdRn8xVbA1cE5fH9jLpT4sW6yZ0u"
          },
          "ownerId": {
            "type": "string",
            "description": "SHA-256 hash (64-char lowercase hex) of the token — the stable, non-secret identifier stored\nagainst the resources you own.\n",
            "pattern": "^[0-9a-f]{64}$",
            "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
          }
        }
      },
      "RandomNamespaceResponse": {
        "type": "object",
        "properties": {
          "namespace": {
            "type": "string",
            "description": "Fresh `<word>-<word>-NN` namespace not currently in use"
          }
        }
      },
      "KvUpsertKeyRequest": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string",
            "description": "New value for the key (max 1 MB)"
          }
        }
      },
      "KvUpsertKeyResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "namespace": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for this key"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access"
          }
        }
      },
      "JsonUpdateResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "namespace": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser viewing"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HtmlUpdateRequest": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "New HTML content to replace existing page"
          }
        }
      },
      "HtmlUpdateResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "namespace": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser viewing"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "HtmlSettingsResponse": {
        "type": "object",
        "description": "Republish-prefill projection returned by `GET /api/html/{ns}/{id}/settings`.\nSettings only, no content body. Owner-token gated.\n",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "description": "10-character short id"
          },
          "ttlDays": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "TTL in days, derived from (expiresAt - createdAt) rounded to whole days"
          },
          "hasPassword": {
            "type": "boolean",
            "description": "True when the page is password-protected. The hash is never returned;\nthe SPA uses this to render a placeholder.\n"
          },
          "showTopBar": {
            "type": "boolean",
            "description": "Per-content toggle for the frontend top toolbar; reflects the stored\nvalue or true if not stored.\n"
          },
          "format": {
            "type": "string",
            "description": "Stored format identifier — 'html', 'markdown', or one of the code-format\nlanguages (yaml, json, xml, csv, tsv, log, toml, ini, sql, sh, bat, env,\njavascript, typescript, tsx, jsx, css, properties, docker, txt).\n"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title extracted at create/update time; null if no title was derivable"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ],
            "description": "Original filename captured at upload, used as tab title fallback and download basename"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "HtmlSourceResponse": {
        "type": "object",
        "description": "Raw user-submitted body returned by `GET /api/html/{ns}/{id}/source`. Owner-only.\nBytes are verbatim — markdown is NOT re-rendered, HTML is NOT re-sanitized.\n",
        "properties": {
          "content": {
            "type": "string",
            "description": "Raw user-submitted content (pre-render, pre-sanitize)"
          },
          "format": {
            "type": "string",
            "description": "Stored format identifier ('html', 'markdown', or a code-format language)"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title extracted at create/update time; null if no title was derivable"
          },
          "namespace": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "description": "10-character short id"
          },
          "ttlDays": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "TTL in days, derived from (expiresAt - createdAt) rounded to whole days"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ],
            "description": "Original filename captured at upload, used as tab title fallback and download basename"
          },
          "showTopBar": {
            "type": "boolean",
            "description": "Per-content toggle for the frontend top toolbar; reflects the stored value or true if not stored"
          }
        }
      },
      "KvCreateStoreRequest": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Initial key name"
          },
          "value": {
            "type": "string",
            "description": "Value for the initial key (max 1 MB)"
          }
        }
      },
      "KvCreateStoreResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric store ID"
          },
          "namespace": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for the initial key"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (upsert/delete)"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ownerToken": {
            "type": "string",
            "description": "Secret token required for upsert and delete operations. Store it safely -- cannot be recovered"
          }
        }
      },
      "JsonCreateResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric document ID"
          },
          "namespace": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser viewing"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (update/delete)"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ownerToken": {
            "type": "string",
            "description": "Secret token required for update and delete operations. Store it safely -- cannot be recovered"
          }
        }
      },
      "HtmlUploadRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "content": {
            "type": "string",
            "description": "HTML or markdown content to publish"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Optional original filename. Used as tab title fallback when no <title>/<h1>\nis present, and as the download basename. Trimmed; rejected if it contains\npath separators, control characters, length > 200, or bare name shorter\nthan 4 chars. Immutable on update.\n"
          },
          "showTopBar": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-page toggle for the BrewPage top toolbar (filename + Download button + theme toggle)\non the served page.\n- true  -> always show the top bar\n- false -> always hide the top bar\n- null/omitted -> use server default (app.ui.show-top-bar-default)\nEffective resolved value is reflected on GET responses via the X-Show-Top-Bar header.\n"
          }
        }
      },
      "HtmlUploadResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric page ID"
          },
          "namespace": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser viewing"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (update/delete)"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ownerToken": {
            "type": "string",
            "description": "Secret token required for update and delete operations. Store it safely -- cannot be recovered"
          },
          "result": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AbuseResult"
              },
              {
                "description": "Outcome envelope. `code` is success|duplicate|warned|failed; `action` is the specific outcome. When a single HTML page references relative sibling files (e.g. `data.js`) — which 404 because single-file pages have no sibling hosting — the page is still created (201) with `action: created_missing_assets`, a human `reason` (e.g. \"This page is broken: it loads files (data.js) that don't exist on a single page. Publish it as a multi-file site so they work.\"), and `extras.localAssetRefs` listing example paths; publish such content as a multi-file site (`POST /api/sites`) instead."
              }
            ]
          }
        }
      },
      "AbuseResult": {
        "type": "object",
        "description": "Outcome envelope returned by content-publishing endpoints (HTML create/update, site republish).\n`code` is the coarse outcome; `action` is the specific outcome. Metadata only.\n",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "success",
              "duplicate",
              "warned",
              "failed"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "created",
              "created_missing_assets",
              "updated",
              "merged",
              "created_warned",
              "unavailable",
              "rate_limited",
              "rejected"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable note (e.g. the created_missing_assets advisory). Omitted when absent."
          },
          "extras": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Outcome-specific extras. created_missing_assets → { \"localAssetRefs\": [\"data.js\", ...] }; merged → { \"isOwner\": boolean }."
          }
        }
      },
      "FileUploadResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric file ID"
          },
          "namespace": {
            "type": "string"
          },
          "filename": {
            "type": "string",
            "description": "Original filename as uploaded"
          },
          "contentType": {
            "type": "string",
            "description": "Detected MIME type (e.g. image/png)"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser download"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (delete)"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ownerToken": {
            "type": "string",
            "description": "Secret token required for delete operation. Store it safely -- cannot be recovered"
          }
        }
      },
      "StatsResponse": {
        "type": "object",
        "properties": {
          "createdToday": {
            "type": "integer",
            "format": "int64",
            "description": "Resources created in the last 24 hours (disjoint sum: alive_public + alive_private + deleted)."
          },
          "totalCreated": {
            "type": "integer",
            "format": "int64",
            "description": "All-time resource count (disjoint sum: alive_public + alive_private + deleted)."
          },
          "viewsToday": {
            "type": "integer",
            "format": "int64",
            "description": "Views in the last 24 hours"
          },
          "totalViews": {
            "type": "integer",
            "format": "int64",
            "description": "All-time view count"
          },
          "breakdown": {
            "type": "array",
            "description": "Per-type breakdown (html, json, kv, file, multi-file site)",
            "items": {
              "$ref": "#/components/schemas/TypeBreakdown"
            }
          },
          "createdTodayPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Alive resources created today in the public namespace (alive only, deleted excluded)."
          },
          "createdTodayPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Alive resources created today in private namespaces (alive only, deleted excluded)."
          },
          "viewsTodayPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Views today on public-namespace resources."
          },
          "viewsTodayPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Views today on private-namespace resources."
          },
          "totalCreatedPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time alive resources in the public namespace (alive only, deleted excluded)."
          },
          "totalCreatedPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time alive resources in private namespaces (alive only, deleted excluded)."
          },
          "totalViewsPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time views on public-namespace resources."
          },
          "totalViewsPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time views on private-namespace resources."
          },
          "deletedToday": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Resources deleted in the last 24 hours via explicit user delete; excludes TTL evictions."
          },
          "deletedTodayPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Resources deleted today in the public namespace."
          },
          "deletedTodayPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Resources deleted today in private namespaces."
          },
          "totalDeleted": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources."
          },
          "totalDeletedPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources in the public namespace."
          },
          "totalDeletedPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources in private namespaces."
          }
        }
      },
      "TypeBreakdown": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Resource type: HTML pages, JSON docs, KV entries, Files, Multi-file sites"
          },
          "today": {
            "type": "integer",
            "format": "int64",
            "description": "Created in the last 24 hours (disjoint sum: alive_public + alive_private + deleted)."
          },
          "total": {
            "type": "integer",
            "format": "int64",
            "description": "All-time count (disjoint sum: alive_public + alive_private + deleted)."
          },
          "todayPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Alive today in the public namespace (alive only, deleted excluded)."
          },
          "todayPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Alive today in private namespaces (alive only, deleted excluded)."
          },
          "totalPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time alive in the public namespace (alive only, deleted excluded)."
          },
          "totalPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time alive in private namespaces (alive only, deleted excluded)."
          },
          "todayDeleted": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Deleted in the last 24 hours via explicit user delete; excludes TTL evictions."
          },
          "todayDeletedPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Deleted today in the public namespace."
          },
          "todayDeletedPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Deleted today in private namespaces."
          },
          "totalDeleted": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources of this type."
          },
          "totalDeletedPublic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources of this type in the public namespace."
          },
          "totalDeletedPrivate": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "All-time count of explicitly deleted resources of this type in private namespaces."
          }
        }
      },
      "KvStoreListResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric store ID"
          },
          "keyCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of keys in the store"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the first key in the store was created"
          }
        }
      },
      "KvListKeysResponse": {
        "type": "object",
        "properties": {
          "keys": {
            "type": "array",
            "description": "All key names in the store",
            "items": {
              "type": "string"
            }
          },
          "count": {
            "type": "integer",
            "format": "int32",
            "description": "Total number of keys"
          }
        }
      },
      "KvGetResponse": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the value was last written or updated"
          }
        }
      },
      "JsonListResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric document ID"
          },
          "content": {
            "type": "string"
          },
          "size": {
            "type": "integer",
            "format": "int64"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GalleryItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Resource type: html, json, kv, file, or site"
          },
          "title": {
            "type": "string",
            "description": "Display title derived from content or filename"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "views": {
            "type": "integer",
            "format": "int64",
            "description": "Total view count"
          }
        }
      },
      "GalleryResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GalleryItem"
            }
          },
          "total": {
            "type": "integer",
            "format": "int64",
            "description": "Total number of matching items across all pages"
          },
          "page": {
            "type": "integer",
            "format": "int32",
            "description": "Current page number (1-based)"
          },
          "size": {
            "type": "integer",
            "format": "int32",
            "description": "Items per page"
          }
        }
      },
      "FileListResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric file ID"
          },
          "filename": {
            "type": "string",
            "description": "Original filename as uploaded"
          },
          "contentType": {
            "type": "string",
            "description": "Detected MIME type (e.g. image/png)"
          },
          "size": {
            "type": "integer",
            "format": "int64"
          },
          "link": {
            "type": "string",
            "description": "Public short URL for browser download"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SiteUploadResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric site ID"
          },
          "namespace": {
            "type": "string"
          },
          "entryFile": {
            "type": "string",
            "description": "Entry HTML file resolved from the upload (e.g. index.html)"
          },
          "link": {
            "type": "string",
            "description": "Public URL to view the site"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (info, delete)"
          },
          "fileCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalSizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ownerToken": {
            "type": "string",
            "description": "Secret token required for info and delete operations. Store it safely -- cannot be recovered"
          }
        }
      },
      "SiteInfoResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric site ID"
          },
          "namespace": {
            "type": "string"
          },
          "entryFile": {
            "type": "string",
            "description": "Entry HTML file resolved from the upload"
          },
          "fileCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalSizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteFileInfo"
            },
            "description": "List of all files in the site"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "views": {
            "type": "integer",
            "format": "int64"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SiteFileInfo": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Relative file path within the site"
          },
          "contentType": {
            "type": "string",
            "description": "Detected MIME type"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "SiteUpdateResponse": {
        "type": "object",
        "description": "Result of a full-replace site republish (`PUT /api/sites/{ns}/{id}`). The URL and ID are unchanged.\nUnlike the create response, no owner token is returned (the caller already holds it).\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric site ID (unchanged)"
          },
          "namespace": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "Public URL to view the site (unchanged)"
          },
          "ownerLink": {
            "type": "string",
            "description": "API URL for programmatic access (info, delete, republish)"
          },
          "entryFile": {
            "type": "string",
            "description": "Entry HTML file resolved from the new upload (e.g. index.html)"
          },
          "fileCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalSizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "result": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AbuseResult"
              },
              {
                "description": "Outcome envelope for the republish (e.g. `{ code: success, action: updated }`)."
              }
            ]
          },
          "visibilityNotice": {
            "type": "string",
            "description": "Optional notice about the site's public visibility (present for public namespace sites)"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error code or short identifier"
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message"
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "description": "HTTP status code"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 timestamp of when the error occurred"
          },
          "supportedTypes": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Present on 415/422 responses from endpoints with raw-body fallback parsing. Lists the Content-Type\nvalues the server accepts for that endpoint (mirrors `FallbackFormatResolver.SUPPORTED_CONTENT_TYPES`).\n"
          }
        }
      },
      "ReportRequest": {
        "type": "object",
        "required": [
          "reportedUrl",
          "category",
          "description"
        ],
        "properties": {
          "reportedUrl": {
            "type": "string",
            "description": "Full URL of the reported resource on brewpage.app or brewdata.app"
          },
          "category": {
            "type": "string",
            "description": "Report category",
            "enum": [
              "spam",
              "phishing",
              "malware",
              "copyright",
              "harassment",
              "illegal",
              "other"
            ]
          },
          "description": {
            "type": "string",
            "minLength": 10,
            "maxLength": 5000,
            "description": "Details of the issue (10..5000 characters)"
          },
          "reporterEmail": {
            "type": "string",
            "format": "email",
            "description": "Optional reporter contact email"
          }
        }
      },
      "ReportResponse": {
        "type": "object",
        "properties": {
          "reportId": {
            "type": "string",
            "description": "Unique 10-character alphanumeric report ID"
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 server timestamp when the report was received"
          }
        }
      },
      "ReportSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric report ID"
          },
          "reportedUrl": {
            "type": "string",
            "description": "Full URL of the reported resource"
          },
          "resourceType": {
            "type": "string",
            "description": "Resource type resolved from the reported URL",
            "enum": [
              "html",
              "kv",
              "json",
              "file",
              "site",
              "unknown"
            ]
          },
          "resourceNamespace": {
            "type": "string",
            "description": "Resolved namespace of the reported resource (if known)"
          },
          "resourceId": {
            "type": "string",
            "description": "Resolved ID of the reported resource (if known)"
          },
          "category": {
            "type": "string",
            "description": "Report category",
            "enum": [
              "spam",
              "phishing",
              "malware",
              "copyright",
              "harassment",
              "illegal",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "description": "Current review status",
            "enum": [
              "new",
              "reviewed",
              "dismissed",
              "actioned"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the report was submitted"
          }
        }
      },
      "ReportDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique 10-character alphanumeric report ID"
          },
          "reportedUrl": {
            "type": "string",
            "description": "Full URL of the reported resource"
          },
          "resourceType": {
            "type": "string",
            "description": "Resource type resolved from the reported URL",
            "enum": [
              "html",
              "kv",
              "json",
              "file",
              "site",
              "unknown"
            ]
          },
          "resourceNamespace": {
            "type": "string",
            "description": "Resolved namespace of the reported resource (if known)"
          },
          "resourceId": {
            "type": "string",
            "description": "Resolved ID of the reported resource (if known)"
          },
          "category": {
            "type": "string",
            "description": "Report category",
            "enum": [
              "spam",
              "phishing",
              "malware",
              "copyright",
              "harassment",
              "illegal",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "description": "Current review status",
            "enum": [
              "new",
              "reviewed",
              "dismissed",
              "actioned"
            ]
          },
          "description": {
            "type": "string",
            "description": "Reporter-provided details of the issue"
          },
          "reporterEmail": {
            "type": "string",
            "description": "Reporter contact email (if provided)"
          },
          "reporterIp": {
            "type": "string",
            "description": "Client IP captured at submission (from X-Forwarded-For)"
          },
          "reporterUserAgent": {
            "type": "string",
            "description": "Client User-Agent captured at submission"
          },
          "reviewerNote": {
            "type": "string",
            "description": "Internal note added by the reviewer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the report was submitted"
          },
          "reviewedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the report was most recently reviewed (null if never reviewed)"
          }
        }
      },
      "ReportUpdateRequest": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "description": "New review status",
            "enum": [
              "new",
              "reviewed",
              "dismissed",
              "actioned"
            ]
          },
          "reviewerNote": {
            "type": "string",
            "description": "Optional reviewer note to attach to the report"
          }
        }
      },
      "AccessEventSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Auto-incremented event ID"
          },
          "resourceType": {
            "type": "string",
            "description": "Resource category (e.g. `html`, `kv`, `json`, `file`, `site`, or other internal label)"
          },
          "resourceId": {
            "type": "string",
            "description": "Resolved resource short ID, if the request targeted a known resource"
          },
          "resourceNamespace": {
            "type": "string",
            "description": "Resolved namespace, if the request targeted a known resource"
          },
          "method": {
            "type": "string",
            "description": "HTTP method (`GET`, `POST`, ...)"
          },
          "path": {
            "type": "string",
            "description": "Request path (without query string)"
          },
          "query": {
            "type": "string",
            "description": "Raw query string, if any"
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "description": "HTTP response status code"
          },
          "durationMs": {
            "type": "integer",
            "format": "int32",
            "description": "Request handling duration, in milliseconds"
          },
          "clientIp": {
            "type": "string",
            "description": "Client IP address (from X-Forwarded-For)"
          },
          "userAgent": {
            "type": "string",
            "description": "Client User-Agent header"
          },
          "referer": {
            "type": "string",
            "description": "Request `Referer` header, if present"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was logged"
          }
        }
      },
      "PathCount": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Request path"
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "Number of requests to this path in the period"
          }
        }
      },
      "IpCount": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "description": "Client IP address"
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "Number of requests from this IP in the period"
          }
        }
      },
      "UaCount": {
        "type": "object",
        "properties": {
          "userAgent": {
            "type": "string",
            "description": "Client User-Agent"
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "Number of requests with this User-Agent in the period"
          }
        }
      },
      "ReferCount": {
        "type": "object",
        "properties": {
          "referer": {
            "type": "string",
            "description": "Referer value"
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "Number of requests with this Referer in the period"
          }
        }
      },
      "AccessStats": {
        "type": "object",
        "properties": {
          "totalRequests": {
            "type": "integer",
            "format": "int64",
            "description": "Total number of requests in the period"
          },
          "uniqueIps": {
            "type": "integer",
            "format": "int64",
            "description": "Number of distinct client IPs in the period"
          },
          "topPaths": {
            "type": "array",
            "description": "Most frequently requested paths, ordered descending by count",
            "items": {
              "$ref": "#/components/schemas/PathCount"
            }
          },
          "topIps": {
            "type": "array",
            "description": "Most active client IPs, ordered descending by count",
            "items": {
              "$ref": "#/components/schemas/IpCount"
            }
          },
          "topUserAgents": {
            "type": "array",
            "description": "Most frequent User-Agents, ordered descending by count",
            "items": {
              "$ref": "#/components/schemas/UaCount"
            }
          },
          "topReferers": {
            "type": "array",
            "description": "Most frequent Referer values, ordered descending by count",
            "items": {
              "$ref": "#/components/schemas/ReferCount"
            }
          },
          "hourlyDistribution": {
            "type": "object",
            "description": "Request count grouped by hour-of-day (0..23). Keys are stringified hour numbers; values are the\nrequest count for that hour across the selected period.\n",
            "additionalProperties": {
              "type": "integer",
              "format": "int32"
            }
          }
        }
      },
      "OwnerInfo": {
        "type": "object",
        "description": "Per-owner resource counts and first-seen timestamp (metadata only).",
        "properties": {
          "ownerId": {
            "type": "string",
            "description": "Hashed owner identifier"
          },
          "htmlCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of HTML pages owned"
          },
          "kvStoreCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of KV stores owned"
          },
          "jsonCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of JSON documents owned"
          },
          "fileCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of files owned"
          },
          "siteCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of multi-file sites owned"
          },
          "firstSeen": {
            "type": "string",
            "format": "date-time",
            "description": "When this owner token was first observed"
          },
          "tier": {
            "type": "string",
            "enum": [
              "DEFAULT",
              "PRIVILEGED"
            ],
            "description": "Owner tier. PRIVILEGED raises the maximum TTL ceiling (up to 730 days)"
          },
          "label": {
            "type": "string",
            "description": "Optional operator-assigned label for the owner"
          }
        }
      },
      "OwnerResolveResponse": {
        "type": "object",
        "description": "Resolution of a raw owner token to its hashed owner ID.",
        "properties": {
          "ownerId": {
            "type": "string",
            "description": "Hashed owner identifier resolved from the supplied token"
          }
        }
      },
      "OwnerPromoteRequest": {
        "type": "object",
        "required": [
          "tier"
        ],
        "description": "Request to set an owner's tier.",
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "DEFAULT",
              "PRIVILEGED"
            ],
            "description": "Target tier"
          },
          "maxTtlDays": {
            "type": "integer",
            "format": "int32",
            "minimum": 1,
            "maximum": 730,
            "description": "Optional max TTL ceiling in days for PRIVILEGED owners (ceiling 730)"
          },
          "label": {
            "type": "string",
            "description": "Optional operator-assigned label for the owner"
          },
          "note": {
            "type": "string",
            "description": "Optional operator note explaining the tier change"
          }
        }
      },
      "OwnerTierResponse": {
        "type": "object",
        "description": "Current tier state of an owner after a promote/demote operation (metadata only).",
        "properties": {
          "ownerId": {
            "type": "string",
            "description": "Hashed owner identifier"
          },
          "tier": {
            "type": "string",
            "enum": [
              "DEFAULT",
              "PRIVILEGED"
            ],
            "description": "Current owner tier"
          },
          "maxTtlDays": {
            "type": "integer",
            "format": "int32",
            "description": "Effective maximum TTL ceiling in days for this owner"
          },
          "label": {
            "type": "string",
            "description": "Operator-assigned label, if any"
          },
          "note": {
            "type": "string",
            "description": "Operator note, if any"
          }
        }
      },
      "AdminStatsResponse": {
        "type": "object",
        "description": "Aggregate platform counts and per-owner breakdown (metadata only).",
        "properties": {
          "totalHtml": {
            "type": "integer",
            "format": "int32",
            "description": "Total HTML pages across the platform"
          },
          "totalKvEntries": {
            "type": "integer",
            "format": "int32",
            "description": "Total KV entries across the platform"
          },
          "totalJsonDocs": {
            "type": "integer",
            "format": "int32",
            "description": "Total JSON documents across the platform"
          },
          "totalFiles": {
            "type": "integer",
            "format": "int32",
            "description": "Total files across the platform"
          },
          "totalSites": {
            "type": "integer",
            "format": "int32",
            "description": "Total multi-file sites across the platform"
          },
          "uniqueOwners": {
            "type": "integer",
            "format": "int32",
            "description": "Number of distinct owner tokens"
          },
          "ownersWithCounts": {
            "type": "array",
            "description": "Per-owner resource breakdown",
            "items": {
              "$ref": "#/components/schemas/OwnerInfo"
            }
          }
        }
      },
      "BlocklistEntryRequest": {
        "type": "object",
        "required": [
          "reason"
        ],
        "description": "Request to add an IP and/or User-Agent pattern to the abuse blocklist.",
        "properties": {
          "ip": {
            "type": "string",
            "description": "Client IP to block (at least one of ip or uaPattern is required)"
          },
          "uaPattern": {
            "type": "string",
            "description": "User-Agent pattern to block (at least one of ip or uaPattern is required)"
          },
          "reason": {
            "type": "string",
            "description": "Human-readable reason for the block"
          },
          "duration": {
            "type": "string",
            "description": "Optional ISO-8601 duration (e.g. `PT1H`, `P1D`). Omit for a permanent block"
          }
        }
      },
      "BlocklistEntryResponse": {
        "type": "object",
        "description": "A single abuse blocklist entry (metadata only).",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Numeric blocklist entry ID"
          },
          "ip": {
            "type": "string",
            "description": "Blocked client IP, if any"
          },
          "uaPattern": {
            "type": "string",
            "description": "Blocked User-Agent pattern, if any"
          },
          "reason": {
            "type": "string",
            "description": "Reason recorded for the block"
          },
          "blockedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the entry was created"
          },
          "blockedBy": {
            "type": "string",
            "description": "Actor that created the entry (e.g. `admin`)"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the block expires (null = permanent)"
          },
          "active": {
            "type": "boolean",
            "description": "Whether the entry is currently in effect"
          }
        }
      },
      "AutoExtendPreviewResponse": {
        "type": "object",
        "description": "Dry-run preview of the TTL auto-extend job (public namespace, metadata only).",
        "properties": {
          "dynamicThreshold": {
            "type": "integer",
            "format": "int64",
            "description": "Computed view threshold used to select candidates"
          },
          "viewFloor": {
            "type": "integer",
            "format": "int64",
            "description": "Configured minimum view floor"
          },
          "viewPercentile": {
            "type": "integer",
            "format": "int32",
            "description": "Configured view percentile"
          },
          "thresholdDays": {
            "type": "integer",
            "format": "int32",
            "description": "Day threshold below which a resource becomes a candidate"
          },
          "candidateCounts": {
            "type": "object",
            "description": "Candidate counts keyed by resource type (html, site, file)",
            "additionalProperties": {
              "type": "integer",
              "format": "int32"
            }
          },
          "sampleIds": {
            "type": "array",
            "description": "Sample of candidate IDs (public namespace only)",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AutoExtendRunResponse": {
        "type": "object",
        "description": "Summary of a TTL auto-extend run.",
        "properties": {
          "extendedCounts": {
            "type": "object",
            "description": "Extended counts keyed by resource type (html, site, file)",
            "additionalProperties": {
              "type": "integer",
              "format": "int32"
            }
          },
          "durationMs": {
            "type": "integer",
            "format": "int64",
            "description": "Wall-clock duration of the run in milliseconds"
          },
          "finalThreshold": {
            "type": "integer",
            "format": "int64",
            "description": "Effective view threshold applied during the run"
          }
        }
      }
    }
  }
}
