Prechádzať zdrojové kódy

docs(api): author openapi.json + llms.txt (multi-project, multi-key)

Fszontagh 2 mesiacov pred
rodič
commit
3b64043a0e
3 zmenil súbory, kde vykonal 896 pridanie a 2 odobranie
  1. 75 1
      api/llms.txt
  2. 810 1
      api/openapi.json
  3. 11 0
      tests/test_api_integration.cpp

+ 75 - 1
api/llms.txt

@@ -1,3 +1,77 @@
 # smartbotic-vectorapi
 
-> REST API fronting smartbotic-database (multi-project, multi-key).
+> General-purpose REST API over smartbotic-database. **Multi-project**: every
+> collection and document lives under a named project supplied as a URL path
+> segment (e.g. `/api/v1/projects/{project}/collections`). **Multi-key**: each
+> API key carries a list of project grants (MySQL-style; `["*"]` for all); admin
+> keys additionally manage projects and keys. Auth: `Authorization: Bearer <KEY>`.
+> Machine-readable spec: `/openapi.json` (OpenAPI 3.1).
+
+## Projects
+
+Requires a valid key. Admin-only operations are noted.
+
+- `GET  /api/v1/projects` — List projects the key may access (admin sees all).
+- `POST /api/v1/projects` `{name}` — Create a project (admin).
+- `GET  /api/v1/projects/{project}` — Get project info (collection/document counts).
+- `DELETE /api/v1/projects/{project}` — Drop a project (admin; the `default` project cannot be dropped).
+
+## Keys (admin)
+
+All key-management endpoints require an admin key.
+
+- `GET  /api/v1/keys` — List keys. Secret value is masked; only `key_prefix`, `label`, `projects`, `admin`, and `created_at` are returned.
+- `POST /api/v1/keys` `{label, projects:[], admin?}` — Generate a new key. Returns the secret key value **once**; store it immediately.
+- `PATCH /api/v1/keys/{key}` `{label?, projects?, admin?}` — Update grants for an existing key.
+- `DELETE /api/v1/keys/{key}` — Revoke a key. Active sessions using it are immediately invalidated.
+
+## Collections
+
+Project-scoped. A key must be granted the project.
+
+- `POST   /api/v1/projects/{project}/collections` `{name, kind:"json"|"vector", vector_dimension?, embedding_model?}` — Create a collection. `vector_dimension` is required for `kind=vector`. `embedding_model` defaults to the server's `default_embedding_model` setting.
+- `GET    /api/v1/projects/{project}/collections` — List collections with metadata.
+- `GET    /api/v1/projects/{project}/collections/{name}` — Get collection metadata.
+- `DELETE /api/v1/projects/{project}/collections/{name}` — Drop a collection and all its documents.
+
+Collection names may not start with `_` or the reserved prefix `vectorapi_`.
+
+## JSON documents
+
+Arbitrary JSON CRUD under a `kind=json` collection.
+
+- `POST  /api/v1/projects/{project}/collections/{name}/documents` `{data:{…}}` — Insert a document. Returns `{id}`.
+- `GET   /api/v1/projects/{project}/collections/{name}/documents/{id}` — Fetch a document by ID.
+- `PUT   /api/v1/projects/{project}/collections/{name}/documents/{id}` — Replace a document (full upsert).
+- `PATCH /api/v1/projects/{project}/collections/{name}/documents/{id}` — Partially update a document (merge fields).
+- `DELETE /api/v1/projects/{project}/collections/{name}/documents/{id}` — Delete a document.
+- `GET   /api/v1/projects/{project}/collections/{name}/documents` — Find documents.
+
+**Filter grammar**: `?filter=field:op:value` (repeatable; ANDed). Supported ops: `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `contains`. Example: `?filter=age:gte:30&filter=name:eq:Alice`.
+
+Additional query params: `limit` (default 20), `offset` (default 0), `sort` (field name), `desc` (bool).
+
+## RAG vectors
+
+Requires a `kind=vector` collection. Vectors are stored with cosine-similarity indexing.
+
+- `POST /api/v1/projects/{project}/collections/{name}/vectors` `{id?, text?, vector?, metadata?}` — Store a vector. Supply either `text` (the server embeds it using the collection's `embedding_model`) or a pre-computed `vector` array. `vector` dimension must match the collection's `vector_dimension`. `metadata` is an arbitrary JSON object stored alongside the vector.
+- `POST /api/v1/projects/{project}/collections/{name}/search` `{query_text?, query_vector?, top_k, min_score?, filters?}` — Cosine-similarity search. Supply either `query_text` or `query_vector`. `top_k` (required) limits results. `min_score` (0–1) filters low-confidence matches. `filters` apply the same `field:op:value` grammar to vector metadata.
+
+## Stats
+
+- `GET /api/v1/projects/{project}/stats` — Per-project stats (collections, document counts). Accessible to any key granted the project.
+- `GET /api/v1/stats` — Server-wide stats including `memory_pressure_level`. Admin only.
+
+## Ops
+
+Public (no auth required):
+
+- `GET /healthz` — Liveness probe; always 200 while the process is running.
+- `GET /readyz`  — Readiness probe; 200 when the database is reachable, 503 otherwise.
+- `GET /openapi.json` — OpenAPI 3.1 machine-readable spec.
+- `GET /llms.txt`     — This file.
+
+Session login (used by the web UI):
+
+- `POST /ui/login` `{key}` — Exchange an API key for an HttpOnly session cookie (`svapi_session`). The cookie may be used in place of the `Authorization: Bearer` header for all `/api/v1/*` endpoints.

+ 810 - 1
api/openapi.json

@@ -1 +1,810 @@
-{ "openapi": "3.1.0", "info": { "title": "smartbotic-vectorapi", "version": "0.1.0" }, "paths": {} }
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "smartbotic-vectorapi",
+    "version": "0.1.0",
+    "description": "General-purpose REST API fronting smartbotic-database. Supports multi-project namespacing and multi-key authorization with per-project grants."
+  },
+  "components": {
+    "securitySchemes": {
+      "bearerAuth": {
+        "type": "http",
+        "scheme": "bearer",
+        "description": "API key issued by the /api/v1/keys endpoint. Pass as Authorization: Bearer <KEY>."
+      }
+    },
+    "schemas": {
+      "Error": {
+        "type": "object",
+        "properties": {
+          "error": { "type": "string" },
+          "message": { "type": "string" }
+        },
+        "required": ["error", "message"]
+      },
+      "CollectionMeta": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "kind": { "type": "string", "enum": ["json", "vector"] },
+          "vector_dimension": { "type": "integer", "minimum": 1 },
+          "embedding_model": { "type": "string" },
+          "created_at": { "type": "integer" }
+        },
+        "required": ["name", "kind"]
+      },
+      "ApiKeyPublic": {
+        "type": "object",
+        "properties": {
+          "key_prefix": { "type": "string" },
+          "label": { "type": "string" },
+          "projects": { "type": "array", "items": { "type": "string" } },
+          "admin": { "type": "boolean" },
+          "created_at": { "type": "integer" }
+        }
+      }
+    }
+  },
+  "security": [{ "bearerAuth": [] }],
+  "paths": {
+    "/healthz": {
+      "get": {
+        "summary": "Liveness check — always 200 if the process is running",
+        "operationId": "healthz",
+        "security": [],
+        "responses": {
+          "200": { "description": "OK" }
+        }
+      }
+    },
+    "/readyz": {
+      "get": {
+        "summary": "Readiness check — 200 when the database is reachable",
+        "operationId": "readyz",
+        "security": [],
+        "responses": {
+          "200": { "description": "Ready" },
+          "503": { "description": "Database not reachable" }
+        }
+      }
+    },
+    "/api/v1/projects": {
+      "get": {
+        "summary": "List projects the key may access (admin sees all)",
+        "operationId": "listProjects",
+        "responses": {
+          "200": {
+            "description": "Project list",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "projects": { "type": "array", "items": { "type": "string" } }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" }
+        }
+      },
+      "post": {
+        "summary": "Create a new project (admin only)",
+        "operationId": "createProject",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "name": { "type": "string" }
+                },
+                "required": ["name"]
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": { "description": "Project created" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" },
+          "422": { "description": "Validation error" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        }
+      ],
+      "get": {
+        "summary": "Get project info (collection and document counts)",
+        "operationId": "getProject",
+        "responses": {
+          "200": {
+            "description": "Project info",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "name": { "type": "string" },
+                    "collections": { "type": "integer" },
+                    "documents": { "type": "integer" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Project not found" }
+        }
+      },
+      "delete": {
+        "summary": "Drop a project (admin only; not 'default')",
+        "operationId": "deleteProject",
+        "responses": {
+          "200": { "description": "Project deleted" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required or cannot drop default" },
+          "404": { "description": "Project not found" }
+        }
+      }
+    },
+    "/api/v1/keys": {
+      "get": {
+        "summary": "List API keys (admin only; secret masked, only key_prefix shown)",
+        "operationId": "listKeys",
+        "responses": {
+          "200": {
+            "description": "Key list",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "keys": {
+                      "type": "array",
+                      "items": { "$ref": "#/components/schemas/ApiKeyPublic" }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" }
+        }
+      },
+      "post": {
+        "summary": "Generate a new API key (admin only); returns the secret key value once",
+        "operationId": "createKey",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "label": { "type": "string" },
+                  "projects": {
+                    "type": "array",
+                    "items": { "type": "string" },
+                    "description": "Project names the key may access; use [\"*\"] for all projects"
+                  },
+                  "admin": { "type": "boolean", "default": false }
+                },
+                "required": ["label", "projects"]
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "Key created; contains the secret key value (shown once)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "key": { "type": "string" },
+                    "label": { "type": "string" },
+                    "projects": { "type": "array", "items": { "type": "string" } },
+                    "admin": { "type": "boolean" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" },
+          "422": { "description": "Validation error" }
+        }
+      }
+    },
+    "/api/v1/keys/{key}": {
+      "parameters": [
+        {
+          "name": "key",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "API key value"
+        }
+      ],
+      "patch": {
+        "summary": "Update key grants (admin only)",
+        "operationId": "updateKey",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "label": { "type": "string" },
+                  "projects": { "type": "array", "items": { "type": "string" } },
+                  "admin": { "type": "boolean" }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": { "description": "Key updated" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" },
+          "404": { "description": "Key not found" }
+        }
+      },
+      "delete": {
+        "summary": "Revoke an API key (admin only)",
+        "operationId": "deleteKey",
+        "responses": {
+          "200": { "description": "Key revoked" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" },
+          "404": { "description": "Key not found" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        }
+      ],
+      "get": {
+        "summary": "List collections in a project",
+        "operationId": "listCollections",
+        "responses": {
+          "200": {
+            "description": "Collection list",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "collections": {
+                      "type": "array",
+                      "items": { "$ref": "#/components/schemas/CollectionMeta" }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" }
+        }
+      },
+      "post": {
+        "summary": "Create a collection in a project",
+        "operationId": "createCollection",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "name": { "type": "string" },
+                  "kind": { "type": "string", "enum": ["json", "vector"] },
+                  "vector_dimension": {
+                    "type": "integer",
+                    "minimum": 1,
+                    "description": "Required when kind=vector"
+                  },
+                  "embedding_model": {
+                    "type": "string",
+                    "description": "Optional; defaults to global default_embedding_model"
+                  }
+                },
+                "required": ["name", "kind"]
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": { "description": "Collection created" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "422": { "description": "Validation error (e.g. missing vector_dimension for vector kind)" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections/{name}": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        },
+        {
+          "name": "name",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Collection name"
+        }
+      ],
+      "get": {
+        "summary": "Get collection metadata",
+        "operationId": "getCollection",
+        "responses": {
+          "200": {
+            "description": "Collection metadata",
+            "content": {
+              "application/json": {
+                "schema": { "$ref": "#/components/schemas/CollectionMeta" }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" }
+        }
+      },
+      "delete": {
+        "summary": "Drop a collection and all its documents",
+        "operationId": "deleteCollection",
+        "responses": {
+          "200": { "description": "Collection deleted" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections/{name}/documents": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        },
+        {
+          "name": "name",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Collection name"
+        }
+      ],
+      "get": {
+        "summary": "Find documents with optional filter, pagination, and sort",
+        "operationId": "findDocuments",
+        "parameters": [
+          {
+            "name": "filter",
+            "in": "query",
+            "schema": { "type": "string" },
+            "description": "Filter expression in field:op:value grammar (e.g. age:gte:30, name:eq:Alice). Multiple filters are ANDed."
+          },
+          {
+            "name": "limit",
+            "in": "query",
+            "schema": { "type": "integer", "default": 20 }
+          },
+          {
+            "name": "offset",
+            "in": "query",
+            "schema": { "type": "integer", "default": 0 }
+          },
+          {
+            "name": "sort",
+            "in": "query",
+            "schema": { "type": "string" },
+            "description": "Field to sort by"
+          },
+          {
+            "name": "desc",
+            "in": "query",
+            "schema": { "type": "boolean", "default": false }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Document list",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "documents": { "type": "array", "items": { "type": "object" } },
+                    "count": { "type": "integer" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" }
+        }
+      },
+      "post": {
+        "summary": "Insert a new JSON document",
+        "operationId": "insertDocument",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "data": {
+                    "type": "object",
+                    "description": "Arbitrary JSON document payload"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "Document inserted",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "id": { "type": "string" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" },
+          "422": { "description": "Validation error" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections/{name}/documents/{id}": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        },
+        {
+          "name": "name",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Collection name"
+        },
+        {
+          "name": "id",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Document ID"
+        }
+      ],
+      "get": {
+        "summary": "Get a document by ID",
+        "operationId": "getDocument",
+        "responses": {
+          "200": {
+            "description": "Document",
+            "content": {
+              "application/json": {
+                "schema": { "type": "object" }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Document not found" }
+        }
+      },
+      "put": {
+        "summary": "Replace a document (full upsert by ID)",
+        "operationId": "replaceDocument",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": { "type": "object" }
+            }
+          }
+        },
+        "responses": {
+          "200": { "description": "Document replaced" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" },
+          "422": { "description": "Validation error" }
+        }
+      },
+      "patch": {
+        "summary": "Partially update a document (merge fields)",
+        "operationId": "patchDocument",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": { "type": "object" }
+            }
+          }
+        },
+        "responses": {
+          "200": { "description": "Document updated" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Document not found" },
+          "422": { "description": "Validation error" }
+        }
+      },
+      "delete": {
+        "summary": "Delete a document by ID",
+        "operationId": "deleteDocument",
+        "responses": {
+          "200": { "description": "Document deleted" },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Document not found" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections/{name}/vectors": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        },
+        {
+          "name": "name",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Collection name (must be kind=vector)"
+        }
+      ],
+      "post": {
+        "summary": "Store a vector; supply either text (auto-embedded) or an explicit vector array",
+        "operationId": "upsertVector",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "id": {
+                    "type": "string",
+                    "description": "Optional stable ID; auto-generated if omitted"
+                  },
+                  "text": {
+                    "type": "string",
+                    "description": "Source text; will be embedded using the collection's embedding_model"
+                  },
+                  "vector": {
+                    "type": "array",
+                    "items": { "type": "number" },
+                    "description": "Pre-computed embedding; dimension must match the collection's vector_dimension"
+                  },
+                  "metadata": {
+                    "type": "object",
+                    "description": "Arbitrary JSON payload stored alongside the vector"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "Vector stored",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "id": { "type": "string" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" },
+          "422": { "description": "Validation error (wrong dimension, not a vector collection, etc.)" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/collections/{name}/search": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        },
+        {
+          "name": "name",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Collection name (must be kind=vector)"
+        }
+      ],
+      "post": {
+        "summary": "Cosine-similarity search; supply either query_text (auto-embedded) or query_vector",
+        "operationId": "searchVectors",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "query_text": {
+                    "type": "string",
+                    "description": "Query text; embedded with the collection's model"
+                  },
+                  "query_vector": {
+                    "type": "array",
+                    "items": { "type": "number" },
+                    "description": "Pre-computed query embedding"
+                  },
+                  "top_k": {
+                    "type": "integer",
+                    "minimum": 1,
+                    "description": "Maximum number of results to return"
+                  },
+                  "min_score": {
+                    "type": "number",
+                    "description": "Minimum cosine similarity score (0–1)"
+                  },
+                  "filters": {
+                    "type": "array",
+                    "items": { "type": "string" },
+                    "description": "Metadata filters in field:op:value grammar (e.g. src:eq:n8n)"
+                  }
+                },
+                "required": ["top_k"]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Search results",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "results": {
+                      "type": "array",
+                      "items": {
+                        "type": "object",
+                        "properties": {
+                          "id": { "type": "string" },
+                          "score": { "type": "number" },
+                          "metadata": { "type": "object" }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" },
+          "404": { "description": "Collection not found" },
+          "422": { "description": "Validation error" }
+        }
+      }
+    },
+    "/api/v1/projects/{project}/stats": {
+      "parameters": [
+        {
+          "name": "project",
+          "in": "path",
+          "required": true,
+          "schema": { "type": "string" },
+          "description": "Project name"
+        }
+      ],
+      "get": {
+        "summary": "Per-project stats (accessible by any key granted the project)",
+        "operationId": "projectStats",
+        "responses": {
+          "200": {
+            "description": "Project stats",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "project": { "type": "string" },
+                    "collections": { "type": "integer" },
+                    "documents": { "type": "integer" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden" }
+        }
+      }
+    },
+    "/api/v1/stats": {
+      "get": {
+        "summary": "Server-wide stats (admin only)",
+        "operationId": "globalStats",
+        "responses": {
+          "200": {
+            "description": "Global stats",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "projects": { "type": "integer" },
+                    "memory_pressure_level": { "type": "integer" },
+                    "collections": { "type": "integer" },
+                    "documents": { "type": "integer" }
+                  }
+                }
+              }
+            }
+          },
+          "401": { "description": "Unauthorized" },
+          "403": { "description": "Forbidden — admin required" }
+        }
+      }
+    }
+  }
+}

+ 11 - 0
tests/test_api_integration.cpp

@@ -153,3 +153,14 @@ TEST_F(ApiFixture, ProjectStatsAndGlobalStats) {
     auto gJson = nlohmann::json::parse(g->body);
     EXPECT_TRUE(gJson.contains("memory_pressure_level"));
 }
+
+TEST_F(ApiFixture, OpenApiDescribesProjectRoutes) {
+    auto r = noAuth().Get("/openapi.json");
+    ASSERT_EQ(r->status, 200);
+    auto j = nlohmann::json::parse(r->body);
+    EXPECT_EQ(j["openapi"], "3.1.0");
+    ASSERT_TRUE(j.contains("paths"));
+    EXPECT_TRUE(j["paths"].contains("/api/v1/projects"));
+    EXPECT_TRUE(j["paths"].contains("/api/v1/projects/{project}/collections/{name}/search"));
+    EXPECT_TRUE(j["paths"].contains("/api/v1/keys"));
+}