{
  "openapi": "3.1.0",
  "info": {
    "title": "Face Registry API",
    "version": "0.1.0",
    "description": "Cloudflare Workers + D1. GET /api/faces samples up to five rows from the first ten matching IDs in ascending order and increments count_found; it always returns next_cursor=null. GET /api/faces/exists checks an exact provider/username/background hash without modifying rows. Neither GET marks used. PATCH /api/faces/toggle-used marks the newest exact match used=true. DELETE /api/faces deletes all face rows matching a required exact username/provider pair, including used rows and duplicates. No upload/reservation APIs. All JSON responses use Cache-Control: no-store. See /docs for human documentation and /llms-full.txt for the complete machine-readable contract. GET /api/faces/manage provides a read-only, paginated management list and total matching count."
  },
  "servers": [
    {
      "url": "https://face-ai.luuanh.online",
      "description": "Current API deployment"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Unauthenticated database connectivity check."
    },
    {
      "name": "Faces",
      "description": "Search, create, read, mark used, and delete face records."
    },
    {
      "name": "Statistics",
      "description": "Global face statistics; UTC daily boundaries."
    },
    {
      "name": "Legacy items",
      "description": "Independent example table; not face data."
    },
    {
      "name": "Dashboard",
      "description": "Static dashboard shell. Data requests still require a token."
    },
    {
      "name": "Documentation",
      "description": "Public documentation and AI discovery. No database queries or authentication required."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getDashboard",
        "tags": [
          "Dashboard"
        ],
        "security": [],
        "summary": "Serve dashboard HTML",
        "responses": {
          "200": {
            "description": "Dashboard shell. Successful API tokens are remembered in origin-scoped localStorage.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Health"
        ],
        "security": [],
        "summary": "Run SELECT 1 on D1",
        "description": "Does not validate table migrations or the API token.",
        "responses": {
          "200": {
            "description": "Database reachable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "const": "ok"
                    },
                    "database": {
                      "const": "connected"
                    }
                  },
                  "required": [
                    "status",
                    "database"
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        }
      }
    },
    "/api/faces": {
      "get": {
        "operationId": "searchFaces",
        "tags": [
          "Faces"
        ],
        "summary": "Sample up to five of the first ten matching IDs",
        "description": "All filters optional and combined using AND. Exact case-sensitive equality for trimmed username/provider. Takes the first ten matching IDs in ascending order, then randomly selects at most five of those candidates and increments count_found exactly once for each returned row. Response order is unspecified; next_cursor is always null. before_id is only an id < bound, not pagination. Use GET /api/faces/exists for reliable existence checks without incrementing count_found.",
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "Trimmed by the server; trimmed length must be 1-255."
            },
            "description": "Exact username; omit to disable this filter."
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "Trimmed by the server; trimmed length must be 1-255."
            },
            "description": "Exact provider; omit to disable this filter."
          },
          {
            "name": "used_status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            },
            "description": "Filter database column used. The key used is not supported in query."
          },
          {
            "name": "before_id",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            },
            "description": "Optional id < before_id bound for the random sample, not a pagination cursor. Positive decimal without leading zeros."
          }
        ],
        "responses": {
          "200": {
            "description": "Zero to five random matches from the first ten matching IDs in ascending order, each with count_found incremented. next_cursor is always null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FacePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "post": {
        "operationId": "createFace",
        "tags": [
          "Faces"
        ],
        "summary": "Insert exactly one face record",
        "description": "Auto-incremented ID. No deduplication or idempotency key; repeating a successful POST can create duplicates. Unknown body fields rejected. used_status can be supplied instead of used; when both exist they must agree after normalization.",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 32768 bytes including JSON encoding. Content-Type application/json required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FaceCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Face created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Face"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Location": {
                "description": "Relative URL of the new resource.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "delete": {
        "operationId": "deleteFacesByIdentity",
        "tags": [
          "Faces"
        ],
        "summary": "Delete all face records for one exact username/provider pair",
        "description": "Both query parameters are required, trimmed, case-sensitive, and combined with AND. Atomically deletes every matching face row, including used rows and duplicates, without sampling or pagination. Returns the actual deleted count; a pair with no matching records or retry returns 200 with deleted_count=0. Rejects missing, blank, overlong, duplicate or unknown query parameters and request bodies. Does not delete legacy items or remote files referenced by link_download. No migration required.",
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": true,
            "description": "Required exact value after trimming; trimmed length must be 1-255. Case-sensitive.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "TRUST"
          },
          {
            "name": "username",
            "in": "query",
            "required": true,
            "description": "Required exact value after trimming; trimmed length must be 1-255. Case-sensitive.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "leson"
          }
        ],
        "responses": {
          "200": {
            "description": "All rows matching the exact pair deleted. Zero matches is a successful no-op.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "provider",
                        "username",
                        "deleted_count"
                      ],
                      "properties": {
                        "provider": {
                          "type": "string"
                        },
                        "username": {
                          "type": "string"
                        },
                        "deleted_count": {
                          "type": "integer",
                          "minimum": 0
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "provider": "TRUST",
                    "username": "leson",
                    "deleted_count": 12
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "405": {
            "description": "Supported methods for /api/faces.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "GET, POST, DELETE"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/faces/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "description": "Positive decimal ID with no leading zero. Malformed route returns 404; unsafe integer on GET/PATCH returns 400."
        }
      ],
      "get": {
        "operationId": "getFace",
        "tags": [
          "Faces"
        ],
        "summary": "Read one face by integer ID",
        "responses": {
          "200": {
            "description": "Face found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Face"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "patch": {
        "operationId": "markFaceUsedById",
        "tags": [
          "Faces"
        ],
        "summary": "Mark this exact record as used",
        "description": "Sets used=true on the exact ID, including older duplicate records. Idempotent; does not change count_found or other fields. Rejects query parameters and unknown body fields. Only used=true is accepted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "used"
                ],
                "properties": {
                  "used": {
                    "type": "boolean",
                    "const": true
                  }
                }
              },
              "example": {
                "used": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Face found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Face"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/stats": {
      "get": {
        "operationId": "getFaceStatistics",
        "tags": [
          "Statistics"
        ],
        "summary": "Global statistics over the face table",
        "description": "No filtering parameters. Seven UTC calendar days including today. Null created_at rows count in totals but not daily activity. providers contains at most eight rows, sorted by total descending then provider ascending. summary.providers is the total distinct count, not the truncated list length.",
        "responses": {
          "200": {
            "description": "Global statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Statistics"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/items": {
      "get": {
        "operationId": "listItems",
        "tags": [
          "Legacy items"
        ],
        "summary": "List legacy items with offset pagination",
        "description": "ORDER BY created_at DESC, id DESC. Unknown query keys are currently ignored. Query values must be decimal integers.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Items page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Item"
                      },
                      "maxItems": 100
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "data",
                    "limit",
                    "offset"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "post": {
        "operationId": "createItem",
        "tags": [
          "Legacy items"
        ],
        "summary": "Create one legacy item",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 32768 bytes including JSON encoding. Content-Type application/json required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Item"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Location": {
                "description": "Relative URL of the new resource.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/items/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9-]+$"
          },
          "description": "Normally the UUID returned by POST /items."
        }
      ],
      "get": {
        "operationId": "getItem",
        "tags": [
          "Legacy items"
        ],
        "summary": "Read legacy item",
        "responses": {
          "200": {
            "description": "Item found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Item"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "put": {
        "operationId": "replaceItem",
        "tags": [
          "Legacy items"
        ],
        "summary": "Replace legacy item name and description",
        "description": "Not PATCH: omitting description clears it to empty string. Updates updated_at and preserves created_at. Extra input fields ignored.",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 32768 bytes including JSON encoding. Content-Type application/json required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Item"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "delete": {
        "operationId": "deleteItem",
        "tags": [
          "Legacy items"
        ],
        "summary": "Delete legacy item",
        "responses": {
          "204": {
            "description": "Deleted; no response body."
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/faces/exists": {
      "get": {
        "operationId": "checkFaceExists",
        "tags": [
          "Faces"
        ],
        "summary": "Check an exact provider, username and background hash",
        "description": "Queries the complete matching set without random sampling, count_found increments or used updates. Both used statuses are included. If duplicates exist, returns the newest matching id. Missing, unknown or duplicate query parameters return 400. No pagination and no migration required.",
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            },
            "description": "Required exact value after trimming. Provider/username are case-sensitive."
          },
          {
            "name": "username",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            },
            "description": "Required exact value after trimming. Provider/username are case-sensitive."
          },
          {
            "name": "sha256_background",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "description": "Required 64-character hexadecimal SHA-256 after trimming; normalized to lowercase."
          }
        ],
        "responses": {
          "200": {
            "description": "Exact existence result; data is a Face when exists=true and null otherwise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FaceExists"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/faces/manage": {
      "get": {
        "operationId": "listManagedFaces",
        "tags": [
          "Faces"
        ],
        "summary": "Browse all face records in read-only pages",
        "description": "Read-only management list ordered by id DESC. Does not increment count_found or change used. Exact username/provider/used_status filters are combined with AND. All parameters are optional; unknown and duplicate parameters are rejected. Offset pagination can shift if records are inserted between requests. filename is a literal substring of the last URL path segment, excluding directories, query strings and fragments. Matches raw or percent-encoded text, ignoring ASCII case; trimmed length 1-255.",
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "Trimmed by the server; trimmed length must be 1-255."
            },
            "description": "Exact username; omit to disable this filter."
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "Trimmed by the server; trimmed length must be 1-255."
            },
            "description": "Exact provider; omit to disable this filter."
          },
          {
            "name": "filename",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            },
            "description": "Partial filename, literal substring; raw and percent-encoded forms supported. ASCII case-insensitive. Combined with other filters using AND."
          },
          {
            "name": "used_status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            },
            "description": "Filter database column used. The key used is not supported in query."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Page size; decimal integer."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "default": 0
            },
            "description": "Number of matching rows to skip; decimal integer."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of matching records and the total matching count. Out-of-range offsets return an empty data array.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagedFacePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/docs": {
      "get": {
        "operationId": "getDocumentation",
        "tags": [
          "Documentation"
        ],
        "summary": "Read the API documentation page",
        "security": [],
        "responses": {
          "200": {
            "description": "Read the API documentation page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "tags": [
          "Documentation"
        ],
        "summary": "Read the full OpenAPI contract",
        "security": [],
        "responses": {
          "200": {
            "description": "Read the full OpenAPI contract",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getAiGuide",
        "tags": [
          "Documentation"
        ],
        "summary": "Discover API documentation and side effects for AI agents",
        "security": [],
        "responses": {
          "200": {
            "description": "Discover API documentation and side effects for AI agents",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "operationId": "getFullAiContract",
        "tags": [
          "Documentation"
        ],
        "summary": "Read the complete OpenAPI contract as text",
        "security": [],
        "responses": {
          "200": {
            "description": "Read the complete OpenAPI contract as text",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/faces/toggle-used": {
      "patch": {
        "operationId": "markFaceUsed",
        "tags": [
          "Faces"
        ],
        "summary": "Mark one exact matching face as used",
        "description": "All three JSON fields are combined with AND. Atomically sets used=true on the matching row with the largest id, even if already used. Never toggles back to false or advances to another duplicate on retry. Changes at most one row, preserving count_found and every other field. Returns 404 when no row matches. Rejects unknown body fields and query parameters. No migration required.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "provider",
                  "username",
                  "sha256_background"
                ],
                "properties": {
                  "provider": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "Required exact value after trimming. Provider/username are case-sensitive."
                  },
                  "username": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "Required exact value after trimming. Provider/username are case-sensitive."
                  },
                  "sha256_background": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64,
                    "description": "Required 64-character hexadecimal SHA-256 after trimming; normalized to lowercase."
                  }
                }
              },
              "example": {
                "provider": "TRUST",
                "username": "leson",
                "sha256_background": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Newest matching face marked used=true; repeated calls keep the same row used.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Face"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "405": {
            "description": "Only PATCH is allowed.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "PATCH"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Exact header Authorization: Bearer <API_TOKEN>. Distinct secrets for dev and production. No token-issuing endpoint."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "StatusInput": {
        "description": "Accepted normalized status values. Strings are case-sensitive and are not trimmed.",
        "oneOf": [
          {
            "type": "boolean"
          },
          {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          },
          {
            "type": "string",
            "enum": [
              "true",
              "false",
              "1",
              "0"
            ]
          }
        ]
      },
      "Face": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "username": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "sha256_background": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "created_at": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 253402300799,
            "description": "Unix seconds, not milliseconds."
          },
          "used": {
            "type": "boolean"
          },
          "link_download": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "description": "HTTP or HTTPS download URL; server does not fetch or check availability."
          },
          "count_found": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of times selected by GET /api/faces. Existence/detail checks do not increment it."
          }
        },
        "required": [
          "id",
          "provider",
          "username",
          "sha256_background",
          "created_at",
          "used",
          "link_download",
          "count_found"
        ]
      },
      "FaceCreate": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "description": "Trimmed by the server; trimmed length must be 1-255."
          },
          "username": {
            "type": "string",
            "minLength": 1,
            "description": "Trimmed by the server; trimmed length must be 1-255."
          },
          "sha256_background": {
            "type": "string",
            "description": "Trimmed, then must contain exactly 64 hexadecimal characters. Normalized to lowercase."
          },
          "created_at": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 253402300799,
            "description": "Unix seconds. Omit to use server time; explicit null is preserved."
          },
          "used": {
            "$ref": "#/components/schemas/StatusInput"
          },
          "used_status": {
            "$ref": "#/components/schemas/StatusInput"
          },
          "link_download": {
            "type": "string",
            "description": "Trimmed, length 1-4096; parsed URL must have protocol http: or https:."
          }
        },
        "required": [
          "provider",
          "username",
          "sha256_background",
          "link_download"
        ],
        "additionalProperties": false,
        "example": {
          "provider": "example",
          "username": "leson",
          "sha256_background": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
          "link_download": "https://example.com/face.jpg",
          "used": false
        }
      },
      "FacePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Face"
            },
            "maxItems": 5
          },
          "limit": {
            "type": "integer",
            "const": 5
          },
          "next_cursor": {
            "type": "null",
            "description": "Always null: random samples have no next page."
          }
        },
        "required": [
          "data",
          "limit",
          "next_cursor"
        ]
      },
      "Statistics": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "minimum": 0
              },
              "used": {
                "type": "integer",
                "minimum": 0
              },
              "unused": {
                "type": "integer",
                "minimum": 0
              },
              "providers": {
                "type": "integer",
                "minimum": 0
              },
              "added_today": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "total",
              "used",
              "unused",
              "providers",
              "added_today"
            ]
          },
          "providers": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "type": "object",
              "properties": {
                "provider": {
                  "type": "string"
                },
                "total": {
                  "type": "integer",
                  "minimum": 0
                },
                "used": {
                  "type": "integer",
                  "minimum": 0
                },
                "unused": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "required": [
                "provider",
                "total",
                "used",
                "unused"
              ]
            }
          },
          "daily": {
            "type": "array",
            "minItems": 7,
            "maxItems": 7,
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "total": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "required": [
                "date",
                "total"
              ]
            }
          },
          "timezone": {
            "type": "string",
            "const": "UTC"
          }
        },
        "required": [
          "summary",
          "providers",
          "daily",
          "timezone"
        ]
      },
      "Item": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "description": {
            "type": "string",
            "maxLength": 5000
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "created_at",
          "updated_at"
        ]
      },
      "ItemInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Trimmed by the server; trimmed length must be 1-200."
          },
          "description": {
            "type": "string",
            "maxLength": 5000,
            "default": ""
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": true,
        "description": "Extra fields are ignored. Description is not trimmed."
      },
      "FaceExists": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "exists": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/Face"
              }
            },
            "required": [
              "exists",
              "data"
            ]
          },
          {
            "type": "object",
            "properties": {
              "exists": {
                "const": false
              },
              "data": {
                "type": "null"
              }
            },
            "required": [
              "exists",
              "data"
            ]
          }
        ]
      },
      "ManagedFacePage": {
        "type": "object",
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Face"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "offset": {
            "type": "integer",
            "minimum": 0
          }
        }
      }
    },
    "responses": {
      "Error400": {
        "description": "Invalid JSON object, field, or query parameter. Returns the first validation error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error401": {
        "description": "Missing or incorrect API token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string",
              "const": "Bearer"
            }
          }
        }
      },
      "Error404": {
        "description": "Route or resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error413": {
        "description": "JSON request body exceeds 32768 bytes.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error415": {
        "description": "Content-Type must be application/json, optionally with charset.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error500": {
        "description": "Unexpected Worker or D1 error; internal details are not returned.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error503": {
        "description": "Worker API_TOKEN is not configured.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}