{
  "openapi": "3.1.0",
  "info": {
    "title": "vaultup API",
    "version": "prod",
    "description": "Single-binary file host for un.vault.moe. Upload (one-shot or resumable), download with per-file delete tokens, auto-delete after N downloads, and per-file expiry (<= 24h). Admin endpoints use a bearer master key."
  },
  "servers": [{ "url": "https://un.vault.moe" }],
  "components": {
    "securitySchemes": {
      "masterKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Operator master key (env VAULTUP_MASTER_KEY). Required for /admin/*."
      }
    },
    "schemas": {
      "FileResult": {
        "type": "object",
        "properties": {
          "hash": { "type": "string", "description": "content xxh3 (hex)" },
          "filename": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "size": { "type": "integer" },
          "dupe": { "type": "boolean" },
          "expires_at": { "type": "integer", "description": "unix seconds" },
          "max_downloads": { "type": ["integer", "null"] },
          "delete_token": { "type": "string", "description": "returned once, at upload" },
          "delete_url": { "type": "string", "format": "uri" }
        }
      },
      "UploadResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "files": { "type": "array", "items": { "$ref": "#/components/schemas/FileResult" } }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "const": false },
          "errorcode": { "type": "integer" },
          "code": { "type": "string", "description": "stable machine-readable slug, e.g. file_too_large" },
          "description": { "type": "string" }
        }
      },
      "FileMeta": {
        "type": "object",
        "properties": {
          "filename": { "type": "string" },
          "originalname": { "type": "string" },
          "size": { "type": "integer" },
          "mime": { "type": "string" },
          "created": { "type": "integer" },
          "expires_at": { "type": "integer" },
          "max_downloads": { "type": ["integer", "null"] },
          "download_count": { "type": "integer" },
          "downloads_remaining": { "type": ["integer", "null"] },
          "available": { "type": "boolean" }
        }
      },
      "APIInfo": {
        "type": "object",
        "properties": {
          "version": { "type": "string" },
          "max_upload_bytes": { "type": "integer" },
          "default_expiry_seconds": { "type": "integer" },
          "max_expiry_seconds": { "type": "integer" },
          "chunk_size": { "type": "integer" },
          "max_chunk_size": { "type": "integer" },
          "rate_limit": { "type": "object" },
          "blocked_extensions": { "type": "array", "items": { "type": "string" } }
        }
      },
      "Generic": {
        "type": "object",
        "properties": { "success": { "type": "boolean" }, "deleted": { "type": "integer" } }
      }
    }
  },
  "paths": {
    "/upload": {
      "post": {
        "summary": "One-shot upload",
        "description": "multipart/form-data with one or more `files[]` parts. Optional query params override defaults.",
        "parameters": [
          { "name": "expire", "in": "query", "schema": { "type": "string" }, "description": "e.g. 6h, 90m (<= 24h)" },
          { "name": "max_downloads", "in": "query", "schema": { "type": "integer" }, "description": "auto-delete after N downloads" },
          { "name": "output", "in": "query", "schema": { "type": "string", "enum": ["json", "csv", "text", "html", "gyazo"] } }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": { "type": "object", "properties": { "files[]": { "type": "array", "items": { "type": "string", "format": "binary" } } } }
            }
          }
        },
        "responses": {
          "200": { "description": "stored", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadResponse" } } } },
          "413": { "description": "file too large", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "415": { "description": "filetype blocked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "rate limited" },
          "507": { "description": "storage full" }
        }
      }
    },
    "/upload/resumable": {
      "post": {
        "summary": "Init a resumable upload session",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["filename", "size"], "properties": { "filename": { "type": "string" }, "size": { "type": "integer" }, "chunk_size": { "type": "integer" }, "expire": { "type": "string" }, "max_downloads": { "type": "integer" } } } } }
        },
        "responses": {
          "201": { "description": "session created", "content": { "application/json": { "schema": { "type": "object", "properties": { "session_id": { "type": "string" }, "session_url": { "type": "string" }, "chunk_size": { "type": "integer" }, "max_chunk_size": { "type": "integer" } } } } } },
          "413": { "description": "file too large" }
        }
      }
    },
    "/upload/resumable/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
      "get": {
        "summary": "Resumable status (which slots are committed)",
        "responses": { "200": { "description": "status", "content": { "application/json": { "schema": { "type": "object", "properties": { "chunk_size": { "type": "integer" }, "received_slots": { "type": "array", "items": { "type": "integer" } }, "complete": { "type": "boolean" } } } } } }, "404": { "description": "unknown session" } }
      },
      "head": {
        "summary": "Contiguous Upload-Offset (legacy sequential resume)",
        "responses": { "200": { "description": "offset in the Upload-Offset header" }, "404": { "description": "unknown session" } }
      },
      "patch": {
        "summary": "Upload one slot at Upload-Offset",
        "description": "Body is the slot bytes (application/offset+octet-stream). Requires Upload-Offset (a slot boundary). Optional Digest: xxh3=<hex> is verified server-side.",
        "responses": { "204": { "description": "slot committed" }, "400": { "description": "bad offset / chunk size" }, "422": { "description": "chunk checksum mismatch" }, "507": { "description": "storage full" } }
      },
      "delete": {
        "summary": "Abort and tear down the session",
        "responses": { "204": { "description": "aborted (idempotent)" } }
      }
    },
    "/upload/resumable/{id}/finish": {
      "post": {
        "summary": "Verify (whole-file xxh3) and publish",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["xxh3"], "properties": { "xxh3": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "published", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadResponse" } } } },
          "400": { "description": "incomplete / checksum required" },
          "409": { "description": "a finish is already in progress" },
          "422": { "description": "checksum mismatch" }
        }
      }
    },
    "/f/{name}": {
      "get": {
        "summary": "Download a file",
        "description": "Supports Range and conditional requests. Emits a strong ETag and Digest: xxh3=<hex>. A request counts against max_downloads only when it receives a fresh full body.",
        "parameters": [{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "file bytes" },
          "206": { "description": "partial (Range)" },
          "304": { "description": "not modified" },
          "404": { "description": "not found" },
          "410": { "description": "expired or download cap reached" },
          "503": { "description": "download concurrency cap reached (Retry-After)" }
        }
      },
      "head": { "summary": "Headers only (never counts)", "parameters": [{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "headers" }, "404": { "description": "not found" } } }
    },
    "/f/{name}/meta": {
      "get": {
        "summary": "Side-effect-free metadata (never counts)",
        "parameters": [{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FileMeta" } } } }, "404": { "description": "not found" } }
      }
    },
    "/delete/{name}": {
      "post": {
        "summary": "Delete a file (per-file token or master key)",
        "description": "Token via X-Delete-Token header, ?token=, or a form field. Wrong-token and not-found both return 403 (no oracle).",
        "parameters": [
          { "name": "name", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "token", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Generic" } } } }, "403": { "description": "forbidden / not found" } }
      },
      "delete": {
        "summary": "Delete a file (per-file token or master key)",
        "parameters": [{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "deleted" }, "403": { "description": "forbidden / not found" } }
      }
    },
    "/api/info": {
      "get": {
        "summary": "Advertise limits and policy (no secrets)",
        "responses": { "200": { "description": "info", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/APIInfo" } } } } }
      }
    },
    "/report": {
      "post": {
        "summary": "Report a file for abuse (unauthenticated)",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string" }, "reason": { "type": "string" } } } } } },
        "responses": { "202": { "description": "queued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Generic" } } } }, "400": { "description": "url required" }, "429": { "description": "rate limited" } }
      }
    },
    "/health": {
      "get": { "summary": "Liveness/readiness", "responses": { "200": { "description": "ok" }, "503": { "description": "unhealthy" } } }
    },
    "/admin/files": {
      "get": {
        "summary": "List all files",
        "security": [{ "masterKey": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer" } },
          { "name": "offset", "in": "query", "schema": { "type": "integer" } },
          { "name": "order", "in": "query", "schema": { "type": "string", "enum": ["created", "size", "downloads"] } },
          { "name": "q", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "listing" }, "401": { "description": "unauthorized" } }
      }
    },
    "/admin/files/{name}": {
      "delete": {
        "summary": "Force-delete any file",
        "security": [{ "masterKey": [] }],
        "parameters": [{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "deleted" }, "401": { "description": "unauthorized" }, "404": { "description": "not found" } }
      }
    },
    "/admin/purge-expired": {
      "post": {
        "summary": "Run an immediate expiry sweep",
        "security": [{ "masterKey": [] }],
        "responses": { "200": { "description": "count removed" }, "401": { "description": "unauthorized" } }
      }
    },
    "/admin/reports": {
      "get": {
        "summary": "List queued abuse reports",
        "security": [{ "masterKey": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer" } },
          { "name": "offset", "in": "query", "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "reports" }, "401": { "description": "unauthorized" } }
      }
    },
    "/admin/reports/{id}": {
      "delete": {
        "summary": "Dismiss a report",
        "security": [{ "masterKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": { "200": { "description": "dismissed" }, "401": { "description": "unauthorized" }, "404": { "description": "not found" } }
      }
    },
    "/admin/blacklist": {
      "get": {
        "summary": "List blacklisted content hashes",
        "security": [{ "masterKey": [] }],
        "responses": { "200": { "description": "entries" }, "401": { "description": "unauthorized" } }
      },
      "post": {
        "summary": "Ingest content hashes (optionally take down matching files)",
        "description": "Hashes are the file's xxh3 hex (as in FileResult.hash). Blocking future uploads of a hash requires BLACKLIST_DB=on; takedown works regardless.",
        "security": [{ "masterKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["hashes"], "properties": { "hashes": { "type": "array", "items": { "type": "string" } }, "takedown": { "type": "boolean" } } } } } },
        "responses": { "200": { "description": "added + taken_down counts" }, "400": { "description": "bad request / bad hash" }, "401": { "description": "unauthorized" } }
      }
    },
    "/admin/blacklist/{hash}": {
      "delete": {
        "summary": "Remove a hash from the blacklist",
        "security": [{ "masterKey": [] }],
        "parameters": [{ "name": "hash", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "removed" }, "401": { "description": "unauthorized" }, "404": { "description": "not found" } }
      }
    }
  }
}
