{
  "openapi": "3.1.0",
  "info": {
    "title": "RepoOps hosted API",
    "version": "1.0.0",
    "summary": "The public HTTP API of repoops.ai: device binding, licensing, telemetry and brain ingest, scoped brain reads, and public product data.",
    "description": "RepoOps is the accountability layer for AI coding agents: it traces every bug back to the prompt that wrote it, keeps a flat-markdown brain that survives session resets, and meters AI coding spend.\n\nWhen to use this API: bind a device or headless sensor to a team (POST /api/connect/exchange), mint and refresh license JWTs (GET /api/license), push telemetry, brain files, and agent traces from a capture agent (the /api/telemetry, /api/brain, /api/agent-traces ingest endpoints), read a user's brain from a third-party integration with a scoped token (GET /api/me/brain/context), or read public product data (marketplace, explore, download feed) with no auth at all.\n\nAuthentication: three bearer schemes, named per operation. A device token binds one machine to one team and is minted by POST /api/connect/exchange from a one-time connect code. A CLI token is a JWT minted by the RFC 8628 device-authorization flow (POST /api/cli/device/code then POST /api/cli/device/token). A read token is a scoped, revocable capability minted in the dashboard (My Brain > Personal read tokens) and is the only credential GET /api/me/brain/context accepts. Send every token as `Authorization: Bearer <token>`, never in a URL.\n\nErrors are JSON. The canonical failure body is `{\"ok\": false, \"code\": \"<stable snake_case token>\", \"error\": \"<human message>\"}`; branch on `code`, never parse `error`. Older endpoints answer the reduced form `{\"error\": \"<human message>\"}`; each operation below documents the shape it actually returns. Exception: POST /api/cli/device/token returns RFC 8628 machine codes (`authorization_pending`, `slow_down`, `expired_token`, `invalid_grant`) in the `error` key, as that RFC requires.\n\nRate limits: ingest endpoints are limited per team and answer 429 with a `retry-after` header; the device-authorization endpoints are limited per IP. Honor `retry-after` before retrying.\n\nThis spec covers the stable public surface. Session-cookie dashboard routes, cron and webhook seams, and enterprise SCIM/SSO (which follow their own RFCs) are intentionally not listed.",
    "contact": {
      "name": "RepoOps",
      "url": "https://www.repoops.ai/contact",
      "email": "support@repoops.ai"
    },
    "termsOfService": "https://www.repoops.ai/terms"
  },
  "externalDocs": {
    "description": "Human-readable API reference and integration guides",
    "url": "https://www.repoops.ai/docs/api"
  },
  "servers": [{ "url": "https://www.repoops.ai" }],
  "tags": [
    { "name": "health", "description": "Liveness" },
    { "name": "device", "description": "Bind, rotate, and revoke a device's team binding" },
    { "name": "cli-auth", "description": "RFC 8628 device-authorization flow for the repoops CLI" },
    { "name": "license", "description": "License JWT mint and offline verification" },
    { "name": "ingest", "description": "Device-token write surface: telemetry, brain files, agent traces" },
    { "name": "brain", "description": "Brain reads" },
    { "name": "requests", "description": "Anonymous feature-request intake and status" },
    { "name": "public-data", "description": "Unauthenticated product data reads" },
    { "name": "distribution", "description": "Desktop installer distribution" }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": ["health"],
        "summary": "Liveness and database probe",
        "description": "Answers 200 when the service and its database respond, 503 when the database probe fails. Returns nothing sensitive. Note: this endpoint's body uses `status`/`db` keys, not the site error shape.",
        "responses": {
          "200": {
            "description": "Service and database are up.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } }
          },
          "503": {
            "description": "Database probe failed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } }
          }
        }
      }
    },
    "/api/connect/exchange": {
      "post": {
        "operationId": "exchangeConnectCode",
        "tags": ["device"],
        "summary": "Exchange a one-time connect code for a device token",
        "description": "Binds this machine to the team that minted the code (dashboard > Connect a device). The code is single-use and short-lived; the response carries the long-lived device token plus the team's license state. Rate limited per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["code", "consent"],
                "properties": {
                  "code": { "type": "string", "description": "The one-time connect code shown in the dashboard." },
                  "consent": { "$ref": "#/components/schemas/Consent" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device bound.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceBinding" } } }
          },
          "400": { "$ref": "#/components/responses/LegacyBadRequest" },
          "404": {
            "description": "Team not found for this code.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/connect/rotate": {
      "post": {
        "operationId": "rotateDeviceToken",
        "tags": ["device"],
        "summary": "Rotate the device token in place",
        "description": "Mints a replacement token for the same binding before the current one expires. Failure bodies carry a machine-readable `reason`: `invalid_token` (401), `token_expired` (403), `token_revoked` (403), or `already_rotated` (409).",
        "security": [{ "deviceToken": [] }],
        "responses": {
          "200": {
            "description": "New token minted; the old one is retired.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["deviceToken", "expiresAt", "teamId"],
                  "properties": {
                    "deviceToken": { "type": "string" },
                    "expiresAt": { "type": "string", "format": "date-time" },
                    "teamId": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/RotateFailure" },
          "403": { "$ref": "#/components/responses/RotateFailure" },
          "409": { "$ref": "#/components/responses/RotateFailure" }
        }
      }
    },
    "/api/connect/disconnect": {
      "post": {
        "operationId": "disconnectDevice",
        "tags": ["device"],
        "summary": "Revoke this device's own binding",
        "security": [{ "deviceToken": [] }],
        "responses": {
          "200": {
            "description": "Binding revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": { "ok": { "type": "boolean" }, "revoked": { "type": "boolean" } },
                  "additionalProperties": true
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" }
        }
      }
    },
    "/api/cli/device/code": {
      "post": {
        "operationId": "startDeviceAuthorization",
        "tags": ["cli-auth"],
        "summary": "Start the RFC 8628 device-authorization flow",
        "description": "Returns a user code and verification URL for the person, and a device code for the machine to poll with. Rate limited to 20 starts per IP per minute.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": { "type": "string", "enum": ["npm", "desktop"], "description": "Which client is asking; shown on the approval page." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Flow started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["device_code", "user_code", "verification_uri", "interval", "expires_in"],
                  "properties": {
                    "device_code": { "type": "string" },
                    "user_code": { "type": "string" },
                    "verification_uri": { "type": "string", "format": "uri" },
                    "verification_uri_complete": { "type": "string", "format": "uri" },
                    "interval": { "type": "integer", "description": "Minimum seconds between polls." },
                    "expires_in": { "type": "integer" }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/cli/device/token": {
      "post": {
        "operationId": "pollDeviceToken",
        "tags": ["cli-auth"],
        "summary": "Poll for the CLI access token",
        "description": "Poll with the device code at the returned interval. Per RFC 8628 the failure body carries machine codes in the `error` key: `authorization_pending` (keep polling), `slow_down` (raise the interval), `expired_token`, `invalid_grant`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["device_code"],
                "properties": { "device_code": { "type": "string" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved; token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["access_token", "token_type", "expires_in"],
                  "properties": {
                    "access_token": { "type": "string" },
                    "token_type": { "type": "string", "const": "Bearer" },
                    "expires_in": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not approved yet, expired, or invalid; see the RFC 8628 code in `error`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": {
                    "error": { "type": "string", "enum": ["authorization_pending", "slow_down", "expired_token", "invalid_grant"] }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/cli/me": {
      "get": {
        "operationId": "getCliIdentity",
        "tags": ["cli-auth"],
        "summary": "Validate a cached CLI token",
        "security": [{ "cliToken": [] }],
        "responses": {
          "200": {
            "description": "Token is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["userId", "email"],
                  "properties": { "userId": { "type": "string" }, "email": { "type": "string", "format": "email" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" }
        }
      }
    },
    "/api/license": {
      "get": {
        "operationId": "mintLicense",
        "tags": ["license"],
        "summary": "Mint a fresh signed license JWT",
        "description": "Answers with an RS256-signed license for the bound team. 402 when the team has no active subscription, 403 when the account is suspended, 503 (with `retryable: true`) when signing is temporarily unavailable.",
        "security": [{ "deviceToken": [] }],
        "responses": {
          "200": {
            "description": "License minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["license", "teamId", "seatCount", "status"],
                  "properties": {
                    "license": { "type": "string", "description": "RS256 JWT; verify offline against GET /api/license/pubkey." },
                    "teamId": { "type": "string" },
                    "seatCount": { "type": "integer" },
                    "status": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" },
          "402": {
            "description": "No active subscription.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "403": {
            "description": "Account suspended.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "503": {
            "description": "License signing unavailable; retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/LegacyError" },
                    { "type": "object", "properties": { "retryable": { "type": "boolean" } } }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/license/pubkey": {
      "get": {
        "operationId": "getLicensePublicKey",
        "tags": ["license"],
        "summary": "Public key for offline license verification",
        "responses": {
          "200": {
            "description": "SPKI PEM public key. Cached for an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["alg", "pubkey"],
                  "properties": { "alg": { "type": "string" }, "pubkey": { "type": "string", "description": "SPKI PEM." } }
                }
              }
            }
          },
          "501": {
            "description": "Signing not configured on this deployment.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          }
        }
      }
    },
    "/api/telemetry/ingest": {
      "post": {
        "operationId": "ingestTelemetry",
        "tags": ["ingest"],
        "summary": "Push a batch of telemetry entries",
        "description": "Append-only, sequence-numbered ingest from the capture agent. A gzip body is accepted with `content-encoding: gzip`. `flagged` reports a sequence gap or rollback the server noticed; `serverRedacted` counts entries the server-side redaction pass touched.",
        "security": [{ "deviceToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["entries"],
                "properties": {
                  "mode": { "type": "string" },
                  "repoKey": { "type": "string" },
                  "entries": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["seq", "payload"],
                      "properties": {
                        "seq": { "type": "integer" },
                        "payload": { "type": "object", "additionalProperties": true },
                        "clientTimestamp": { "type": "string", "format": "date-time" }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "accepted", "maxSeq"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "accepted": { "type": "integer" },
                    "maxSeq": { "type": "integer" },
                    "flagged": { "type": ["string", "null"], "enum": ["gap", "rollback", null] },
                    "serverRedacted": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/LegacyBadRequest" },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" },
          "402": { "$ref": "#/components/responses/TierGate" },
          "413": {
            "description": "Batch too large.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/telemetry/heartbeat": {
      "post": {
        "operationId": "telemetryHeartbeat",
        "tags": ["ingest"],
        "summary": "Signal the capture agent is alive",
        "security": [{ "deviceToken": [] }],
        "responses": {
          "200": {
            "description": "Heartbeat recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "at"],
                  "properties": { "ok": { "type": "boolean" }, "at": { "type": "string", "format": "date-time" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" }
        }
      }
    },
    "/api/brain/ingest": {
      "post": {
        "operationId": "ingestBrain",
        "tags": ["ingest"],
        "summary": "Push a versioned snapshot of brain files",
        "description": "Uploads the tracked repo's flat-markdown brain. 422 means the raw-secret tripwire matched a file and the batch was refused; redact and resend.",
        "security": [{ "deviceToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["version", "files"],
                "properties": {
                  "repoKey": { "type": "string" },
                  "version": { "type": "integer" },
                  "clientTimestamp": { "type": "string", "format": "date-time" },
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["path", "content"],
                      "properties": { "path": { "type": "string" }, "content": { "type": "string" } }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Snapshot stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "version", "files"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "version": { "type": "integer" },
                    "files": { "type": "integer" },
                    "bytes": { "type": "integer" },
                    "redactionHits": { "type": "integer" },
                    "ledger": {
                      "type": "object",
                      "properties": { "gap": { "type": "boolean" }, "rollback": { "type": "boolean" } }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/LegacyBadRequest" },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" },
          "402": { "$ref": "#/components/responses/TierGate" },
          "413": {
            "description": "Snapshot too large.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "422": {
            "description": "Raw-secret tripwire refused the batch.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          }
        }
      }
    },
    "/api/brain/search": {
      "get": {
        "operationId": "searchBrain",
        "tags": ["brain"],
        "summary": "Search the bound team's brain",
        "security": [{ "deviceToken": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "The search query."
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked hits over the team's stored brain files.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "additionalProperties": true, "description": "Ranked hit list; fields include the matching file path and a snippet per hit." }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" }
        }
      }
    },
    "/api/agent-traces/ingest": {
      "post": {
        "operationId": "ingestAgentTraces",
        "tags": ["ingest"],
        "summary": "Push recorded agent sessions and spans",
        "security": [{ "deviceToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["sessions"],
                "properties": {
                  "repoKey": { "type": "string" },
                  "clientTimestamp": { "type": "string", "format": "date-time" },
                  "sessions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["outcome", "spans"],
                      "properties": {
                        "outcome": { "type": "string" },
                        "spans": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                      },
                      "additionalProperties": true
                    }
                  },
                  "labels": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Traces stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "sessions": { "type": "integer" },
                    "spans": { "type": "integer" },
                    "labels": { "type": "integer" },
                    "serverRedacted": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/LegacyBadRequest" },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" },
          "402": { "$ref": "#/components/responses/TierGate" },
          "413": {
            "description": "Batch too large.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          }
        }
      }
    },
    "/api/policy": {
      "get": {
        "operationId": "getCapturePolicy",
        "tags": ["ingest"],
        "summary": "Server-owned capture policy for this device",
        "description": "The capture-mode floor, watchlist, and approval requirements the team has set. The capture agent reads this before every session.",
        "security": [{ "deviceToken": [] }],
        "responses": {
          "200": {
            "description": "Current policy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "captureMode"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "captureMode": { "type": "string" },
                    "watchlist": { "type": "array", "items": { "type": "string" } },
                    "actions": {
                      "type": "object",
                      "properties": { "requireApprovals": { "type": "boolean" } },
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/LegacyUnauthorized" }
        }
      }
    },
    "/api/me/brain/context": {
      "get": {
        "operationId": "readBrainContext",
        "tags": ["brain"],
        "summary": "Read cited slices of a user's brain with a scoped token",
        "description": "The third-party integration surface (see https://www.repoops.ai/docs/integration-kit). Accepts ONLY a personal read token, minted and revoked in the dashboard; session cookies and other token kinds are rejected. Every read is audited for the token's owner. Per-token daily read cap answers 429.",
        "security": [{ "readToken": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "The question to retrieve context for."
          }
        ],
        "responses": {
          "200": {
            "description": "Cited slices, filtered to the token's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["query", "total", "shown", "hits"],
                  "properties": {
                    "query": { "type": "string" },
                    "total": { "type": "integer" },
                    "shown": { "type": "integer" },
                    "hits": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["filePath", "snippet"],
                        "properties": {
                          "filePath": { "type": "string" },
                          "kind": { "type": "string" },
                          "rank": { "type": "number" },
                          "snippet": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, or revoked token.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/requests/ingest": {
      "post": {
        "operationId": "submitRequest",
        "tags": ["requests"],
        "summary": "Submit an anonymous product request",
        "description": "Public intake, no auth. Bodies over 8 KB are refused; submissions are rate limited per IP and the IP is stored only as a hash.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "additionalProperties": true, "description": "The request text plus optional context fields." }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. Keep the id to poll status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id"],
                  "properties": { "ok": { "type": "boolean" }, "id": { "type": "string", "pattern": "^req_" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/OkFalseBadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/requests/status": {
      "get": {
        "operationId": "getRequestStatus",
        "tags": ["requests"],
        "summary": "Poll the status of submitted requests",
        "description": "Anonymous: knowing an id is the capability. Up to 100 comma-separated `req_` ids per call.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "Comma-separated request ids, each matching ^req_[\\w-]{1,128}$."
          }
        ],
        "responses": {
          "200": {
            "description": "Status per known id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "statuses"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "statuses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["id", "status"],
                        "properties": { "id": { "type": "string" }, "status": { "type": "string" } }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/OkFalseBadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/marketplace/public": {
      "get": {
        "operationId": "listPublicMarketplace",
        "tags": ["public-data"],
        "summary": "List public brain packs, trust-ranked",
        "responses": {
          "200": {
            "description": "Public marketplace entries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "entries", "total"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "entries": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error, canonical shape.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/explore": {
      "get": {
        "operationId": "explorePublicBrains",
        "tags": ["public-data"],
        "summary": "Featured, trending, and new public brains",
        "description": "Unauthenticated read used by the /explore page. Degrades to empty sections rather than erroring.",
        "responses": {
          "200": {
            "description": "Public brain listings by section.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "additionalProperties": true, "description": "Sections of public brain summaries (featured, trending, new, byStack)." }
              }
            }
          }
        }
      }
    },
    "/api/download/latest": {
      "get": {
        "operationId": "getLatestInstaller",
        "tags": ["distribution"],
        "summary": "Resolve the latest desktop installer",
        "description": "The resolver `npx repoops` calls. 404s carry a `download` fallback URL a person can open.",
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["win32", "win"], "default": "win32" },
            "description": "Only Windows builds ship today; other platforms answer 404 with a fallback link."
          }
        ],
        "responses": {
          "200": {
            "description": "Latest installer pointer. Cached five minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["url", "version", "sha512"],
                  "properties": {
                    "url": { "type": "string", "format": "uri" },
                    "version": { "type": "string" },
                    "sha512": { "type": "string" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unsupported platform or no build available; `error` is `unsupported-platform` or `not-available`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/LegacyError" },
                    { "type": "object", "properties": { "download": { "type": "string", "format": "uri" } } }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "deviceToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Opaque device token binding one machine to one team. Mint with POST /api/connect/exchange; rotate with POST /api/connect/rotate."
      },
      "cliToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "CLI access token from the RFC 8628 flow (POST /api/cli/device/code, then POST /api/cli/device/token)."
      },
      "readToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Scoped, revocable personal read token minted in the dashboard (My Brain > Personal read tokens). The only credential /api/me/brain/context accepts."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The canonical failure body. Branch on `code`; `error` is for a person and its wording can change.",
        "required": ["ok", "code", "error"],
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "code": {
            "type": "string",
            "description": "Stable snake_case token, e.g. bad_request, unauthorized, payment_required, forbidden, not_found, conflict, too_large, unprocessable, rate_limited, server_error, unavailable."
          },
          "error": { "type": "string" }
        }
      },
      "LegacyError": {
        "type": "object",
        "description": "The reduced failure body older endpoints return.",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      },
      "Health": {
        "type": "object",
        "required": ["status", "db", "ts"],
        "properties": {
          "status": { "type": "string", "enum": ["ok", "degraded"] },
          "db": { "type": "string", "enum": ["up", "down"] },
          "ts": { "type": "string", "format": "date-time" }
        }
      },
      "Consent": {
        "type": "object",
        "required": ["version"],
        "properties": {
          "version": { "type": "string", "description": "The consent text version the operator accepted." },
          "deviceLabel": { "type": "string" }
        }
      },
      "DeviceBinding": {
        "type": "object",
        "required": ["deviceToken", "teamId", "teamName", "status", "seatCount", "consentVersion"],
        "properties": {
          "deviceToken": { "type": "string" },
          "deviceTokenExpiresAt": { "type": ["string", "null"], "format": "date-time" },
          "license": { "type": ["string", "null"], "description": "Signed license JWT when the team has one." },
          "teamId": { "type": "string" },
          "teamName": { "type": "string" },
          "status": { "type": "string" },
          "seatCount": { "type": "integer" },
          "consentVersion": { "type": "string" }
        },
        "additionalProperties": true
      }
    },
    "responses": {
      "LegacyUnauthorized": {
        "description": "Missing, invalid, expired, or revoked bearer token.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
      },
      "LegacyBadRequest": {
        "description": "Malformed request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
      },
      "OkFalseBadRequest": {
        "description": "Malformed request.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["ok", "error"],
              "properties": { "ok": { "type": "boolean", "const": false }, "error": { "type": "string" } }
            }
          }
        }
      },
      "TierGate": {
        "description": "The team's tier does not include this capability; `entitled` names what would.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/LegacyError" },
                { "type": "object", "properties": { "entitled": {} } }
              ]
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit hit; honor Retry-After.",
        "headers": {
          "Retry-After": { "schema": { "type": "string" }, "description": "Seconds to wait before retrying." }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyError" } } }
      },
      "RotateFailure": {
        "description": "Rotation refused; branch on `reason`.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/LegacyError" },
                {
                  "type": "object",
                  "required": ["reason"],
                  "properties": {
                    "reason": { "type": "string", "enum": ["invalid_token", "token_expired", "token_revoked", "already_rotated"] }
                  }
                }
              ]
            }
          }
        }
      }
    }
  }
}
