{
  "openapi": "3.1.0",
  "info": {
    "title": "Publr Agent API",
    "version": "0.1.0",
    "description": "Agent-facing Publr API for creating and listing agent tokens, checking the current token principal, and publishing one inline file to a Publr sandbox URL. This specification documents only implemented routes."
  },
  "servers": [
    {
      "url": "https://api.publr.host",
      "description": "Publr API"
    }
  ],
  "tags": [
    {
      "name": "Agent tokens",
      "description": "Clerk-authenticated token management."
    },
    {
      "name": "Agent publishing",
      "description": "Agent-token-authenticated publishing."
    },
    {
      "name": "Agent self-signup",
      "description": "Unauthenticated, rate-limited, enumeration-proof account provisioning via an emailed one-time code."
    }
  ],
  "paths": {
    "/api/v1/agent/tokens": {
      "get": {
        "tags": ["Agent tokens"],
        "operationId": "listAgentTokens",
        "summary": "List agent tokens",
        "description": "Lists caller-owned agent token previews for the authenticated Clerk user. Raw token secrets and token hashes are never returned.",
        "security": [{ "ClerkBearer": [] }],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Convex pagination cursor returned by a previous response.",
            "schema": { "type": "string" }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Number of tokens to return. Defaults to 20 and is capped at 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated caller-owned agent token previews.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListAgentTokensResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/AuthRequired" }
        }
      },
      "post": {
        "tags": ["Agent tokens"],
        "operationId": "createAgentToken",
        "summary": "Create an agent token",
        "description": "Creates a revocable Publr agent token for the authenticated Clerk user. The raw token secret is returned once.",
        "security": [{ "ClerkBearer": [] }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateAgentTokenRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Agent token created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateAgentTokenResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/AuthRequired" },
          "500": { "$ref": "#/components/responses/ConfigMissing" }
        }
      }
    },
    "/api/v1/agent/tokens/{id}/revoke": {
      "post": {
        "tags": ["Agent tokens"],
        "operationId": "revokeAgentToken",
        "summary": "Revoke an agent token",
        "description": "Revokes a caller-owned agent token by Convex id.",
        "security": [{ "ClerkBearer": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RevokeAgentTokenRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Agent token revoked.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RevokeAgentTokenResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/AuthRequired" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/agent/whoami": {
      "get": {
        "tags": ["Agent publishing"],
        "operationId": "agentWhoami",
        "summary": "Inspect the current agent token principal",
        "description": "Returns the user, token preview, and Convex-auth proof for a valid agent token.",
        "security": [{ "AgentBearer": [] }],
        "responses": {
          "200": {
            "description": "Current agent principal.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AgentWhoamiResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/AuthRequired" },
          "500": { "$ref": "#/components/responses/AgentConvexAuthFailed" }
        }
      }
    },
    "/api/v1/agent/publish": {
      "post": {
        "tags": ["Agent publishing"],
        "operationId": "agentPublish",
        "summary": "Publish one inline file",
        "description": "Creates one project, writes one inline UTF-8 file to R2, runs sync security checks, completes the existing upload flow, and returns a sandbox URL.",
        "security": [{ "AgentBearer": [] }],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "UUID v4 replay key for this publish request.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AgentPublishRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Published sandbox URL.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AgentPublishResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/AuthRequired" },
          "403": { "$ref": "#/components/responses/AuthForbidden" },
          "409": { "$ref": "#/components/responses/SlugUnavailable" },
          "413": { "$ref": "#/components/responses/FileTooLarge" },
          "415": { "$ref": "#/components/responses/BlockedContent" },
          "500": { "$ref": "#/components/responses/ConfigMissing" }
        }
      }
    },
    "/api/v1/agent/auth/request-code": {
      "post": {
        "tags": ["Agent self-signup"],
        "operationId": "requestAgentSignupCode",
        "summary": "Request an agent self-signup code",
        "description": "Emails a one-time verification code to the supplied address. UNAUTHENTICATED. Enumeration-proof: returns a byte-identical 202 whether or not the email already has a Publr account, and the response is held to a constant-timing floor so account existence cannot be inferred from latency. Rate-limited per IP (minute/hour/day) and per email-hash (hour); honour the Retry-After header on 429.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RequestSignupCodeRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The code was accepted for delivery. Always returned for any well-formed email, regardless of account state.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RequestSignupCodeResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ConfigMissing" }
        }
      }
    },
    "/api/v1/agent/auth/verify-code": {
      "post": {
        "tags": ["Agent self-signup"],
        "operationId": "verifyAgentSignupCode",
        "summary": "Verify a self-signup code and mint the first agent token",
        "description": "Verifies the emailed code, creates-or-finds the Clerk user, seeds the Publr account, and mints the user's first agent token. UNAUTHENTICATED. Every bad-code reason (not found / expired / already used / wrong / locked) collapses to one generic 400 invalid_code so nothing about the code or account leaks. The apiKey is a standard revocable pk_live_* agent token and is returned exactly once.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/VerifySignupCodeRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account provisioned; the first agent token is returned once.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VerifySignupCodeResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidSignupCode" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ConfigMissing" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ClerkBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Clerk JWT",
        "description": "Clerk session JWT for token management routes."
      },
      "AgentBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Publr agent token",
        "description": "Publr agent token beginning with pk_live_ or pk_test_."
      }
    },
    "responses": {
      "ValidationError": {
        "description": "Input validation failed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "AuthRequired": {
        "description": "Authentication required.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "AuthForbidden": {
        "description": "Authentication succeeded but the caller is not allowed to perform the operation.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "NotFound": {
        "description": "Requested resource was not found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "ConfigMissing": {
        "description": "Required server-side dynamic configuration is missing.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "AgentConvexAuthFailed": {
        "description": "Agent token was valid, but the internal Convex auth probe failed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "SlugUnavailable": {
        "description": "Requested slug is already reserved or deployed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "FileTooLarge": {
        "description": "Inline content exceeds the authenticated user's plan limit.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "BlockedContent": {
        "description": "Sync security checks blocked the uploaded bytes.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlockedContentResponse" } } }
      },
      "RateLimited": {
        "description": "Too many requests. Honour the Retry-After header before retrying.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "InvalidSignupCode": {
        "description": "The verification code is invalid, expired, already used, or locked. Every reason returns this one generic body so nothing about the code or account state leaks.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      }
    },
    "schemas": {
      "AgentTokenScope": {
        "type": "string",
        "enum": ["publish:write", "tokens:manage"]
      },
      "RequestSignupCodeRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email to send the one-time code to. Normalised (trimmed + lowercased) server-side."
          },
          "language": {
            "type": "string",
            "description": "Optional locale for the verification email (e.g. en-US, pt-BR). Defaults to en-US."
          }
        }
      },
      "RequestSignupCodeResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status"],
        "properties": {
          "status": { "type": "string", "enum": ["code_sent"] }
        }
      },
      "VerifySignupCodeRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["email", "code"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The same email the code was requested for."
          },
          "code": {
            "type": "string",
            "description": "The one-time code from the email — a human-readable ABC-234-shaped string. Trimmed and uppercased server-side, so a pasted lowercase code still matches."
          }
        }
      },
      "VerifySignupCodeResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["apiKey", "tokenPreview", "clerkUserId", "created"],
        "properties": {
          "apiKey": {
            "type": "string",
            "description": "The raw pk_live_* agent token. A standard revocable agent token, returned exactly once — store it now."
          },
          "tokenPreview": {
            "type": "string",
            "description": "A redacted preview of the token, safe to log or display."
          },
          "clerkUserId": {
            "type": "string",
            "description": "The Clerk user id backing the account."
          },
          "created": {
            "type": "boolean",
            "description": "true when a new Clerk user was created; false when an existing account was matched."
          }
        }
      },
      "CreateAgentTokenRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "label": { "type": "string", "minLength": 1 },
          "scopes": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AgentTokenScope" }
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateAgentTokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["tokenId", "token", "publicId", "tokenPreview", "tokenPrefix", "tokenSuffix", "scopes", "status", "createdAt", "expiresAt"],
        "properties": {
          "tokenId": { "type": "string" },
          "token": { "type": "string" },
          "publicId": { "type": "string" },
          "tokenPreview": { "type": "string" },
          "tokenPrefix": { "type": "string", "enum": ["pk_live_", "pk_test_"] },
          "tokenSuffix": { "type": "string" },
          "label": { "type": "string" },
          "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/AgentTokenScope" } },
          "status": { "type": "string", "enum": ["active"] },
          "createdAt": { "type": "number" },
          "expiresAt": { "type": ["number", "null"] }
        }
      },
      "RevokeAgentTokenRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": { "type": "string", "minLength": 1 }
        }
      },
      "RevokeAgentTokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["tokenId", "status", "revokedAt", "alreadyRevoked"],
        "properties": {
          "tokenId": { "type": "string" },
          "status": { "type": "string", "enum": ["revoked"] },
          "revokedAt": { "type": ["number", "null"] },
          "alreadyRevoked": { "type": "boolean" }
        }
      },
      "ListAgentTokensResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["page", "isDone", "continueCursor"],
        "properties": {
          "page": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ListAgentTokenPreview" }
          },
          "isDone": { "type": "boolean" },
          "continueCursor": { "type": "string" }
        }
      },
      "ListAgentTokenPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": ["tokenId", "publicId", "tokenPreview", "tokenPrefix", "tokenSuffix", "scopes", "status", "createdAt"],
        "properties": {
          "tokenId": { "type": "string" },
          "publicId": { "type": "string" },
          "tokenPreview": { "type": "string" },
          "tokenPrefix": { "type": "string", "enum": ["pk_live_", "pk_test_"] },
          "tokenSuffix": { "type": "string" },
          "label": { "type": "string" },
          "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/AgentTokenScope" } },
          "status": { "type": "string", "enum": ["active", "revoked"] },
          "createdAt": { "type": "number" },
          "expiresAt": { "type": "number" },
          "revokedAt": { "type": "number" }
        }
      },
      "AgentWhoamiResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["user", "token", "convexAuth"],
        "properties": {
          "user": { "$ref": "#/components/schemas/AgentUser" },
          "token": { "$ref": "#/components/schemas/AgentTokenPreview" },
          "convexAuth": { "$ref": "#/components/schemas/ConvexAuthProof" }
        }
      },
      "AgentUser": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "email", "role", "plan", "status"],
        "properties": {
          "id": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "role": { "type": "string" },
          "plan": { "type": "string" },
          "status": { "type": "string" }
        }
      },
      "AgentTokenPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "publicId", "tokenPreview", "scopes", "expiresAt"],
        "properties": {
          "id": { "type": "string" },
          "publicId": { "type": "string" },
          "tokenPreview": { "type": "string" },
          "label": { "type": "string" },
          "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/AgentTokenScope" } },
          "expiresAt": { "type": ["number", "null"] }
        }
      },
      "ConvexAuthProof": {
        "type": "object",
        "additionalProperties": false,
        "required": ["userId", "clerkUserId", "subject"],
        "properties": {
          "userId": { "type": "string" },
          "clerkUserId": { "type": "string" },
          "subject": { "type": "string" }
        }
      },
      "AgentPublishRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "slug", "filename", "contentType", "content", "sessionFingerprint", "language"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 120 },
          "slug": { "type": "string", "minLength": 3, "maxLength": 50, "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" },
          "filename": { "type": "string", "minLength": 1, "maxLength": 255 },
          "contentType": { "type": "string", "minLength": 1, "maxLength": 255 },
          "content": { "type": "string", "minLength": 1 },
          "sessionFingerprint": { "type": "string", "minLength": 1, "maxLength": 128 },
          "language": { "type": "string", "enum": ["pt-BR", "en-US"] }
        }
      },
      "AgentPublishResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["projectId", "workspaceId", "assetId", "deploymentId", "hostname", "sandboxUrl"],
        "properties": {
          "projectId": { "type": "string" },
          "workspaceId": { "type": "string" },
          "assetId": { "type": "string" },
          "deploymentId": { "type": "string" },
          "hostname": { "type": "string" },
          "sandboxUrl": { "type": "string", "format": "uri" }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": true,
        "required": ["error", "correlationId"],
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string" },
          "correlationId": { "type": "string" }
        }
      },
      "BlockedContentResponse": {
        "type": "object",
        "additionalProperties": true,
        "required": ["error", "code", "userMessage", "alternative", "correlationId"],
        "properties": {
          "error": { "type": "string", "enum": ["validation/blocked"] },
          "code": { "type": "string" },
          "userMessage": { "type": "object", "additionalProperties": true },
          "alternative": { "type": ["object", "null"], "additionalProperties": true },
          "correlationId": { "type": "string" }
        }
      }
    }
  }
}
