{
  "openapi": "3.1.0",
  "info": {
    "title": "tiinyapp.farm",
    "version": "0.1.17",
    "summary": "The reviewed catalog of small apps for the Tiiny AI Pocket Lab.",
    "description": "Everything under /api/, plus the account, maker and file routes, is answered by a Cloudflare Worker; everything else is a static file. Responses are JSON with Cache-Control: no-store unless said otherwise.\n\nAny POST, PUT, PATCH or DELETE that does not carry a bearer token on a route that accepts one must send Origin: https://tiinyapp.farm, or it is refused with 403.\n\nEvery failure is a JSON object with an error field holding a sentence meant for a person followed by the one-line agent support pointer. Warnings carry the same pointer. The health route also reports its component booleans. An unexpected failure answers 502 with a general sentence rather than the underlying text, because that text can carry credentials.",
    "license": {
      "name": "MIT",
      "url": "https://github.com/Titanium-Devops/tiinyapp-farm/blob/main/LICENSE"
    }
  },
  "servers": [
    {
      "url": "https://tiinyapp.farm"
    }
  ],
  "externalDocs": {
    "description": "The written guide for assistants",
    "url": "https://tiinyapp.farm/docs/agents/"
  },
  "tags": [
    {
      "name": "Health"
    },
    {
      "name": "Sign in"
    },
    {
      "name": "Prove you own a Tiiny"
    },
    {
      "name": "Your account"
    },
    {
      "name": "Images"
    },
    {
      "name": "Art in the farm's hand"
    },
    {
      "name": "Apps"
    },
    {
      "name": "Releases"
    },
    {
      "name": "Seeds and comments"
    },
    {
      "name": "The catalog"
    },
    {
      "name": "Files"
    },
    {
      "name": "Pages the Worker serves"
    }
  ],
  "paths": {
    "/api/health": {
      "x-farm-source": "index.mjs",
      "get": {
        "summary": "Check the farm services",
        "description": "Reads one fixed KV key and checks the launcher feed in R2. A missing key or object is healthy; only a failed service call is not. The response is never cached and needs no credential.",
        "security": [],
        "responses": {
          "200": {
            "description": "KV and R2 answered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "KV or R2 did not answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for the health check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Health"
        ]
      },
      "head": {
        "summary": "Check the farm services without a body",
        "description": "Runs the same KV and R2 checks as GET and returns only the status and no-store headers.",
        "security": [],
        "responses": {
          "200": {
            "description": "KV and R2 answered."
          },
          "503": {
            "description": "KV or R2 did not answer."
          },
          "405": {
            "description": "Use GET or HEAD for the health check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Health"
        ]
      }
    },
    "/api/auth/start": {
      "x-farm-source": "index.mjs",
      "post": {
        "summary": "Send a sign-in code",
        "description": "Emails a six digit code that lasts ten minutes and dies after five wrong attempts.",
        "security": [],
        "responses": {
          "200": {
            "description": "The code was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "const": true
                    }
                  },
                  "required": [
                    "sent"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "That is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The Origin header is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Three codes an hour is the limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The mail service failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Email sign-in is not configured yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Sign in"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "3 per hour per address, 10 per hour per client, 100 per hour across the farm"
      }
    },
    "/api/auth/verify": {
      "x-farm-source": "index.mjs",
      "post": {
        "summary": "Finish signing in with a code",
        "description": "Sets the session cookie. Signed in already, this links the address to that account instead of making a second one.",
        "security": [],
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "user"
                  ]
                }
              }
            },
            "headers": {
              "Set-Cookie": {
                "description": "__Host-farm, HttpOnly, Secure, SameSite=Lax, 30 days.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The code has expired or is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Finish linking from the account that asked for the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Sign in"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "pattern": "^\\d{6}$"
                  }
                },
                "required": [
                  "email",
                  "code"
                ]
              }
            }
          }
        }
      }
    },
    "/api/auth/github": {
      "x-farm-source": "index.mjs",
      "get": {
        "summary": "Start signing in with GitHub",
        "description": "Redirects to GitHub's authorize page.",
        "security": [],
        "responses": {
          "302": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "GitHub sign-in is not configured yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Sign in"
        ]
      }
    },
    "/api/auth/github/callback": {
      "x-farm-source": "index.mjs",
      "get": {
        "summary": "Finish signing in with GitHub",
        "description": "GitHub sends the visitor back here. On success it redirects to /submit/ and sets the session cookie.",
        "security": [],
        "responses": {
          "302": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "GitHub sign-in was not completed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The sign-in expired or the state did not match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "GitHub did not answer as expected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Sign in"
        ],
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/auth/logout": {
      "x-farm-source": "index.mjs",
      "post": {
        "summary": "Sign out",
        "description": "Ends this session and clears the cookie.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "signedOut": {
                      "const": true
                    }
                  },
                  "required": [
                    "signedOut"
                  ]
                }
              }
            }
          }
        },
        "tags": [
          "Sign in"
        ]
      }
    },
    "/api/me": {
      "x-farm-source": "index.mjs",
      "get": {
        "summary": "Who is signed in",
        "description": "Answers the account on this cookie, or null when there is none. A bearer token is ignored here.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/User"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "user"
                  ]
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ]
      }
    },
    "/api/tokens": {
      "x-farm-source": "index.mjs",
      "post": {
        "summary": "Create an API token",
        "description": "The full token is in this answer and nowhere else afterwards. Creating one needs the cookie, so a token cannot mint another token.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "Copy the token now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "pattern": "^farm_[a-f0-9]{40}$"
                    },
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "createdAt": {
                      "type": "string"
                    },
                    "lastUsedAt": {
                      "type": "null"
                    }
                  },
                  "required": [
                    "token",
                    "id",
                    "name",
                    "prefix"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Give the token a name of 80 characters or fewer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Verify you own a Tiiny first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Five tokens is the limit. Revoke one first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "5 live tokens per account"
      },
      "get": {
        "summary": "List your API tokens",
        "description": "Their names and prefixes, never the tokens.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Token"
                      }
                    }
                  },
                  "required": [
                    "tokens"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ]
      }
    },
    "/api/tokens/{id}": {
      "x-farm-source": "index.mjs",
      "delete": {
        "summary": "Revoke an API token",
        "description": "The token stops working at once.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "const": true
                    }
                  },
                  "required": [
                    "revoked"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such token on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The token id from GET /api/tokens, not the token.",
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{32}$"
            },
            "example": "00000000000000000000000000000000"
          }
        ]
      }
    },
    "/api/social/counts": {
      "x-farm-source": "social.mjs",
      "get": {
        "summary": "Every app's seeds and comments in one answer",
        "description": "One read for a whole catalog page, so a grid of apps does not ask once per tile. Reading needs no credential and the answer is cached for 60 seconds. Makers are keyed by handle and carry the seeds their apps have been given, added up. A maker who hid their page is not listed.",
        "security": [],
        "responses": {
          "200": {
            "description": "The seed and comment counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialCounts"
                }
              }
            }
          },
          "405": {
            "description": "That action does not use this method.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The catalog is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ]
      }
    },
    "/api/seeds/{id}/social": {
      "x-farm-source": "social.mjs",
      "get": {
        "summary": "Read an app's seeds and comments",
        "description": "Reading needs no credential. mine is true when the signed-in visitor has given this app a seed. thumbs carries the same number as seeds, under the name the first release used. A farm token is optional; one that matches nobody is refused with 401.",
        "security": [
          {
            "session": []
          },
          {
            "farmToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Social"
                }
              }
            }
          },
          "401": {
            "description": "That farm token is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "That action does not use this method.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Comment page, newest page first, with at most 50 comments.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ]
      }
    },
    "/api/seeds/{id}/seed": {
      "x-farm-source": "social.mjs",
      "post": {
        "summary": "Give this app a seed",
        "description": "One seed per person per app. Giving it again takes it back. The same route as /api/seeds/{id}/thumb, under the name a person reads.",
        "security": [
          {
            "session": []
          },
          {
            "farmToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation, with your seed given or taken back.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Social"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ]
      }
    },
    "/api/seeds/{id}/thumb": {
      "x-farm-source": "social.mjs",
      "post": {
        "summary": "Give this app a seed, under the first release's name",
        "description": "The same route as /api/seeds/{id}/seed. Giving it again takes it back.",
        "security": [
          {
            "session": []
          },
          {
            "farmToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation, with your seed given or taken back.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Social"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ]
      }
    },
    "/api/seeds/{id}/comments": {
      "x-farm-source": "social.mjs",
      "post": {
        "summary": "Leave a comment",
        "description": "Needs a verified Tiiny profile. Deleting comments does not give the hourly limit back.",
        "security": [
          {
            "session": []
          },
          {
            "farmToken": []
          }
        ],
        "responses": {
          "201": {
            "description": "Posted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Social"
                }
              }
            }
          },
          "400": {
            "description": "Write 1 to 1000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Verify you own a Tiiny first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This conversation already retains 200 comments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Five comments an hour is the limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 1000
                  }
                },
                "required": [
                  "text"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "5 per hour per account"
      }
    },
    "/api/seeds/{id}/comments/{commentId}": {
      "x-farm-source": "social.mjs",
      "delete": {
        "summary": "Remove a comment",
        "description": "Its author or a farm admin.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Social"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only the author or a farm admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog, or that comment is not here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Seeds and comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "description": "The id of the comment, from the conversation.",
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{32}$"
            },
            "example": "00000000000000000000000000000000"
          }
        ]
      }
    },
    "/api/maker": {
      "x-farm-source": "makers.mjs",
      "post": {
        "summary": "Save your maker profile",
        "description": "The bio, the links and the avatar shown on your maker page.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "user"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A field is too long, an unknown link, or an image that is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bio": {
                    "type": "string",
                    "maxLength": 600
                  },
                  "links": {
                    "type": "object",
                    "properties": {
                      "github": {
                        "type": "string"
                      },
                      "website": {
                        "type": "string"
                      },
                      "youtube": {
                        "type": "string"
                      }
                    },
                    "additionalProperties": false
                  },
                  "avatarKey": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "bio",
                  "links"
                ]
              }
            }
          }
        }
      }
    },
    "/api/maker/visibility": {
      "x-farm-source": "makers.mjs",
      "put": {
        "summary": "Show or hide your maker page",
        "description": "Hidden leaves it reachable to signed-in visitors only.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "public": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "public"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Choose true or false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Your account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "public"
                ]
              }
            }
          }
        }
      }
    },
    "/api/media": {
      "x-farm-source": "makers.mjs",
      "post": {
        "summary": "Upload an image",
        "description": "The type is read from the file's own first bytes, not its name. Keep the URL that comes back and put it in the submission form.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "key",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Verify you own a Tiiny first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Larger than 2 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Not a PNG, JPEG or WebP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Images"
        ],
        "requestBody": {
          "required": true,
          "description": "The image bytes.",
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "x-farm-rate-limit": "2 MiB per image"
      }
    },
    "/api/media/{key}": {
      "x-farm-source": "makers.mjs",
      "delete": {
        "summary": "Delete one of your images",
        "description": "Only your own.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "const": true
                    }
                  },
                  "required": [
                    "deleted"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "That image is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Images"
        ],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Your account id and the image name. Use the url the upload answered with rather than assembling this yourself.",
            "schema": {
              "type": "string"
            },
            "example": "sample-maker/00000000000000000000000000000000.png"
          }
        ]
      }
    },
    "/api/owners": {
      "x-farm-source": "proof.mjs",
      "get": {
        "summary": "Look up a verified owner",
        "description": "The public lookup the pull request checks use. It needs no credential and skips the Worker's request queue, so it stays quick during an upload.",
        "security": [],
        "responses": {
          "200": {
            "description": "Whether that profile is verified here.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "verified": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "verified",
                    "name"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "That is not a TiinyVerse profile URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Prove you own a Tiiny"
        ],
        "parameters": [
          {
            "name": "profile",
            "in": "query",
            "required": true,
            "description": "https://www.tiinyverse.com/users/<uuid>",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ]
      }
    },
    "/api/tiinyverse/link": {
      "x-farm-source": "proof.mjs",
      "post": {
        "summary": "Ask for a bio code",
        "description": "Put the code anywhere in your TiinyVerse bio, save the profile, then call verify. The code lasts 24 hours.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The code and what to do with it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "profileUrl": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    },
                    "expires": {
                      "type": "integer"
                    },
                    "instruction": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "profileUrl",
                    "code",
                    "expires",
                    "instruction"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "That is not a TiinyVerse profile URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already verified, or that profile belongs to another account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Prove you own a Tiiny"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "profileUrl": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "profileUrl"
                ]
              }
            }
          }
        }
      }
    },
    "/api/tiinyverse/verify": {
      "x-farm-source": "proof.mjs",
      "post": {
        "summary": "Check the bio code",
        "description": "Reads your public profile, looks for the code as a whole word, and takes your display name from the page.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Verified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tiinyverse": {
                      "$ref": "#/components/schemas/Tiinyverse"
                    }
                  },
                  "required": [
                    "tiinyverse"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Ask for a new code; the last one expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That profile belongs to another account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The profile could not be read, or the code is not on it yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Prove you own a Tiiny"
        ]
      }
    },
    "/api/seeds/{id}/art": {
      "x-farm-source": "art.mjs",
      "get": {
        "summary": "The art the farm last drew",
        "description": "The pair drawn for this app, the scene it was drawn from, and how many drawings are left today.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The last pair, or empty strings when there is none.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Art"
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Verify you own a Tiiny, and the app id must be yours or unclaimed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or POST for app art.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Art in the farm's hand"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ]
      },
      "post": {
        "summary": "Draw a header and an icon",
        "description": "One sentence describing the scene. A drawing takes a minute or two, so give the call a generous timeout. A refusal or a failure costs you nothing.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "Both images are stored and answered as URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Art"
                }
              }
            }
          },
          "400": {
            "description": "The scene is empty, too long, or not one line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "This app id belongs to another maker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The farm is still drawing this app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The drawing service refused those words.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The app, maker, or farm daily drawing limit was reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The drawing could not be made or kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Drawing app art is not switched on yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "The drawing took too long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Art in the farm's hand"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scene": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 200
                  }
                },
                "required": [
                  "scene"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "3 per app, 12 per maker, and 30 across the farm per day; one drawing at a time per app"
      }
    },
    "/api/seeds/{id}/release-check": {
      "x-farm-source": "release.mjs",
      "post": {
        "summary": "Check GitHub for a newer release",
        "description": "Asks GitHub for the newest full release of the repository in the manifest, downloads that archive, measures its checksum and size here, and opens or refreshes one pull request for this app. The session cookie only: a bearer token is not accepted on this route.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "What the check found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseCheck"
                }
              }
            }
          },
          "429": {
            "description": "Checked a moment ago. The message says how long to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseCheck"
                }
              }
            }
          },
          "401": {
            "description": "Sign in first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only this app's verified maker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use POST to check for a new release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The release archive could not be measured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Release checks are not switched on yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Releases"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ],
        "x-farm-rate-limit": "1 per minute per app"
      }
    },
    "/api/seeds/mine": {
      "x-farm-source": "seeds.mjs",
      "get": {
        "summary": "Your submissions and their checks",
        "description": "Every app you have submitted with its state, its checks, its reviews and its release tracking. This is what farm status <id> reads.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your apps.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "seeds": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Seed"
                      }
                    }
                  },
                  "required": [
                    "seeds"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Apps"
        ]
      }
    },
    "/api/seeds": {
      "x-farm-source": "seeds.mjs",
      "post": {
        "summary": "Submit an app",
        "description": "Validates the manifest, measures the archive, and opens a pull request on the catalog repository. Nothing is published until a maintainer merges it. Send a release URL or an upload, never both.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "The pull request is open.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "prUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "statusUrl": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "statusUrl"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Saved, but one step did not finish. The warning says what to do next.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "statusUrl": {
                      "type": "string"
                    },
                    "warning": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "statusUrl",
                    "warning"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A field is wrong, or both a release URL and an upload were sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Verify you own a Tiiny first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "408": {
            "description": "The upload took too long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That app id belongs to another maker, or is already in the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Larger than 50 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Send the form as multipart/form-data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The release link did not answer 200 with a gzip archive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Five submissions an hour is the limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "GitHub could not finish the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "App submission is not configured yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Apps"
        ],
        "requestBody": {
          "required": true,
          "description": "The catalog text, the runtime needs, and either a release URL or one upload.",
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
                  },
                  "name": {
                    "type": "string"
                  },
                  "pitch": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "license": {
                    "type": "string"
                  },
                  "tags": {
                    "type": "string",
                    "description": "Comma separated. The site sends one category."
                  },
                  "permissions": {
                    "type": "string",
                    "description": "Comma separated: microphone, files, network, device."
                  },
                  "command": {
                    "type": "string",
                    "description": "A start command."
                  },
                  "entry": {
                    "type": "string",
                    "description": "JSON entry object, or the string null for a library."
                  },
                  "repo": {
                    "type": "string"
                  },
                  "homepage": {
                    "type": "string"
                  },
                  "video": {
                    "type": "string"
                  },
                  "media": {
                    "type": "string",
                    "description": "JSON of uploaded icon, header and gallery URLs."
                  },
                  "screenshots": {
                    "type": "string",
                    "description": "JSON array of uploaded image URLs."
                  },
                  "python": {
                    "type": "string"
                  },
                  "ports": {
                    "type": "string"
                  },
                  "models": {
                    "type": "string"
                  },
                  "npuUnits": {
                    "type": "string"
                  },
                  "health": {
                    "type": "string"
                  },
                  "selfcheck": {
                    "type": "string",
                    "enum": [
                      "true"
                    ]
                  },
                  "releaseUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "archive": {
                    "type": "string",
                    "format": "binary",
                    "description": "A gzip tar archive up to 50 MB."
                  }
                },
                "required": [
                  "id",
                  "name",
                  "pitch",
                  "description",
                  "version",
                  "license"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "5 per hour per account"
      }
    },
    "/api/seeds/{id}": {
      "x-farm-source": "seeds.mjs",
      "put": {
        "summary": "Update an app you own",
        "description": "The same form. The id cannot change. A release change needs a strictly newer version; text alone may keep the current one.",
        "security": [
          {
            "farmToken": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "The pull request is open.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "prUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "statusUrl": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "statusUrl"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Saved, but one step did not finish. The warning says what to do next.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "statusUrl": {
                      "type": "string"
                    },
                    "warning": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "statusUrl",
                    "warning"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A field is wrong, or the version is not newer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign in or send a token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only this app's verified maker can update it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "That app is not in the catalog yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Larger than 50 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Send the form as multipart/form-data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Five submissions an hour is the limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Apps"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The catalog text, the runtime needs, and either a release URL or one upload.",
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
                  },
                  "name": {
                    "type": "string"
                  },
                  "pitch": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "license": {
                    "type": "string"
                  },
                  "tags": {
                    "type": "string",
                    "description": "Comma separated. The site sends one category."
                  },
                  "permissions": {
                    "type": "string",
                    "description": "Comma separated: microphone, files, network, device."
                  },
                  "command": {
                    "type": "string",
                    "description": "A start command."
                  },
                  "entry": {
                    "type": "string",
                    "description": "JSON entry object, or the string null for a library."
                  },
                  "repo": {
                    "type": "string"
                  },
                  "homepage": {
                    "type": "string"
                  },
                  "video": {
                    "type": "string"
                  },
                  "media": {
                    "type": "string",
                    "description": "JSON of uploaded icon, header and gallery URLs."
                  },
                  "screenshots": {
                    "type": "string",
                    "description": "JSON array of uploaded image URLs."
                  },
                  "python": {
                    "type": "string"
                  },
                  "ports": {
                    "type": "string"
                  },
                  "models": {
                    "type": "string"
                  },
                  "npuUnits": {
                    "type": "string"
                  },
                  "health": {
                    "type": "string"
                  },
                  "selfcheck": {
                    "type": "string",
                    "enum": [
                      "true"
                    ]
                  },
                  "releaseUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "archive": {
                    "type": "string",
                    "format": "binary",
                    "description": "A gzip tar archive up to 50 MB."
                  }
                },
                "required": [
                  "id",
                  "name",
                  "pitch",
                  "description",
                  "version",
                  "license"
                ]
              }
            }
          }
        },
        "x-farm-rate-limit": "5 per hour per account"
      }
    },
    "/account/": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "Your apps",
        "description": "The page that shows your submissions, your profile and your API tokens. Signed out it redirects to /submit/.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/makers/{handle}/": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "A maker's public page",
        "description": "Built live from the catalog. A maker who has turned their page off is reachable to signed-in visitors only.",
        "security": [],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "That maker was not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The maker's handle.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
            },
            "example": "sample-maker"
          }
        ]
      }
    },
    "/makers/{handle}/card.png": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "A maker's share card",
        "description": "The 1200 by 630 image social sites show.",
        "security": [],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Share card not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for share cards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The maker's handle.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
            },
            "example": "sample-maker"
          }
        ]
      }
    },
    "/media/{key}": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "An uploaded image",
        "description": "Immutable, cached for a year.",
        "security": [],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Image not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for images.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Your account id and the image name. Use the url the upload answered with rather than assembling this yourself.",
            "schema": {
              "type": "string"
            },
            "example": "sample-maker/00000000000000000000000000000000.png"
          }
        ]
      }
    },
    "/seeds-files/{id}/{version}/{name}": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "An archive uploaded through the site",
        "description": "The release archive for an app whose maker uploaded one rather than linking a GitHub release.",
        "security": [],
        "responses": {
          "200": {
            "description": "The archive.",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "That app file does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for app files.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d+\\.\\d+\\.\\d+$"
            },
            "example": "0.1.0"
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The archive filename, ending .tar.gz.",
            "schema": {
              "type": "string"
            },
            "example": "sample-app-0.1.0.tar.gz"
          }
        ]
      }
    },
    "/launcher/latest.json": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "The launcher update feed",
        "description": "What the Tiiny App Farm desktop launcher reads to learn whether a newer version of itself exists. The shape is the Tauri updater's: a version, the day it was published, and one entry per platform holding a signed archive url. It is never cached, and until the first launcher release it answers 404.",
        "security": [],
        "responses": {
          "200": {
            "description": "The feed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string",
                      "description": "major.minor.patch."
                    },
                    "notes": {
                      "type": "string"
                    },
                    "pub_date": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "platforms": {
                      "type": "object",
                      "description": "Keyed by darwin-aarch64, darwin-x86_64 and windows-x86_64.",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "signature": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          }
                        },
                        "required": [
                          "signature",
                          "url"
                        ]
                      }
                    }
                  },
                  "required": [
                    "version",
                    "platforms"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The launcher has not been published yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for launcher downloads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ]
      }
    },
    "/launcher/releases.json": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "The launcher release history",
        "description": "Every launcher version that has shipped, newest first, with its date, its notes and its files. The site's version history page is built from this at deploy time, and it is never cached. Until the first release publishes it, it answers 404.",
        "security": [],
        "responses": {
          "200": {
            "description": "The history.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "releases": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "version": {
                            "type": "string"
                          },
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "commit": {
                            "type": "string"
                          },
                          "notes": {
                            "type": "string",
                            "description": "Markdown."
                          },
                          "files": {
                            "type": "object",
                            "description": "Keyed by mac, windows and linux; each a list of files.",
                            "additionalProperties": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "size": {
                                    "type": "integer"
                                  },
                                  "sha256": {
                                    "type": "string"
                                  },
                                  "signed": {
                                    "type": "boolean"
                                  },
                                  "notarised": {
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "name",
                                  "size",
                                  "sha256"
                                ]
                              }
                            }
                          }
                        },
                        "required": [
                          "version",
                          "date",
                          "files"
                        ]
                      }
                    }
                  },
                  "required": [
                    "releases"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The launcher release history has not been published yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for launcher downloads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ]
      }
    },
    "/launcher/versions/": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The version history page",
        "description": "Every launcher version, what changed in it, and a link to each file with its size, its checksum and whether it is signed. A page of the static site rather than a file in the bucket, so the Worker hands it to the site.",
        "security": [],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/launcher/{file}": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "A launcher download",
        "description": "The disk image, the installer, or the signed archive an installed launcher updates itself from. A filename carrying a version is cached for a year, because that name can never hold different bytes.",
        "security": [],
        "responses": {
          "200": {
            "description": "The file.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "That launcher file does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Use GET or HEAD for launcher downloads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ],
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "One launcher filename. Take it from the feed rather than assembling it yourself, because the version is part of the name.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
            },
            "example": "Tiiny-App-Farm_0.1.0_universal.dmg"
          }
        ]
      }
    },
    "/plant": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "An old path",
        "description": "301 to /install/.",
        "security": [],
        "responses": {
          "301": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/seeds": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "An old path",
        "description": "301 to /submit/.",
        "security": [],
        "responses": {
          "301": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/farm": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "An old path",
        "description": "301 to /account/.",
        "security": [],
        "responses": {
          "301": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/seeds/mine": {
      "x-farm-source": "makers.mjs",
      "get": {
        "summary": "An old path",
        "description": "301 to /account/.",
        "security": [],
        "responses": {
          "301": {
            "description": "A redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Pages the Worker serves"
        ]
      }
    },
    "/catalog.json": {
      "x-farm-source": "assets",
      "get": {
        "summary": "Every manifest in one file",
        "description": "The whole catalog, and the newest farm version, in one file. Read this first when you want to see what the farm has. A deploy in flight can still be serving the older shape, which is the apps array on its own.",
        "security": [],
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cli": {
                      "type": "string",
                      "description": "The newest tiinyapp-farm on PyPI, as the site knows it."
                    },
                    "apps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Manifest"
                      }
                    }
                  },
                  "required": [
                    "cli",
                    "apps"
                  ]
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/manifests/{id}.json": {
      "x-farm-source": "assets",
      "get": {
        "summary": "One app manifest",
        "description": "The file the installer reads before it downloads anything.",
        "security": [],
        "responses": {
          "200": {
            "description": "The manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Manifest"
                }
              }
            }
          },
          "404": {
            "description": "No such app.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The app id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
            },
            "example": "sample-app"
          }
        ]
      }
    },
    "/categories.json": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The tag to category map",
        "description": "How the site groups tags, and the order it shows them in.",
        "security": [],
        "responses": {
          "200": {
            "description": "The map and the order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "map": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "order": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "map",
                    "order"
                  ]
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/llms.txt": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The index for assistants",
        "description": "A short plain-text index that names the agent guide, this document, and the pages behind them. Fetch it first.",
        "security": [],
        "responses": {
          "200": {
            "description": "The index.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/support/llms.txt": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The support index for agents",
        "description": "What the farm is, self-serve support links, status, security and privacy rules, contact channels and support hours.",
        "security": [],
        "responses": {
          "200": {
            "description": "The support index.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/support/agent.md": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The support procedure for agents",
        "description": "The ordered diagnostic and drafting procedure an agent follows before a person submits a support request.",
        "security": [],
        "responses": {
          "200": {
            "description": "The support procedure.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/agent.txt": {
      "x-farm-source": "main.mjs",
      "get": {
        "summary": "Discover Agent View Layer companions",
        "description": "Names the public page companion pattern and the related agent, support, OpenAPI, sitemap and robots resources.",
        "security": [],
        "responses": {
          "200": {
            "description": "The discovery manifest.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/docs/openapi.json": {
      "x-farm-source": "assets",
      "get": {
        "summary": "This document",
        "description": "The whole HTTP surface as OpenAPI 3.1.",
        "security": [],
        "responses": {
          "200": {
            "description": "This document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    },
    "/docs/manifest.schema.json": {
      "x-farm-source": "assets",
      "get": {
        "summary": "The manifest schema",
        "description": "The JSON Schema both the site and the pull request checks validate a manifest against.",
        "security": [],
        "responses": {
          "200": {
            "description": "The schema.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "tags": [
          "The catalog"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "farmToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A token you create on https://tiinyapp.farm/account/ and copy once. It is farm_ followed by 40 hexadecimal characters. Eight routes accept one: POST /api/seeds, PUT /api/seeds/<id>, POST /api/media, GET /api/seeds/mine, GET and POST /api/seeds/<id>/art, POST /api/seeds/<id>/seed and its older name /thumb, and POST /api/seeds/<id>/comments. Everywhere else it is ignored and the cookie decides, so a token cannot mint another token."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-farm",
        "description": "Set by signing in with an email code or with GitHub. HttpOnly, Secure, SameSite=Lax, 30 days. Signing in again replaces the previous session."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "One sentence meant for a person to read."
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "Health": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "kv": {
            "type": "boolean"
          },
          "r2": {
            "type": "boolean"
          },
          "version": {
            "type": "string",
            "description": "The deployed Worker version when available."
          },
          "error": {
            "type": "string",
            "description": "Names the failed service without exposing details."
          }
        },
        "required": [
          "ok",
          "kv",
          "r2"
        ],
        "additionalProperties": false
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "email": {
            "type": "string"
          },
          "github": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "login": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string"
              }
            }
          },
          "tiinyverse": {
            "$ref": "#/components/schemas/Tiinyverse"
          },
          "handle": {
            "type": [
              "string",
              "null"
            ]
          },
          "bio": {
            "type": "string"
          },
          "avatarKey": {
            "type": [
              "string",
              "null"
            ]
          },
          "links": {
            "type": "object",
            "properties": {
              "github": {
                "type": "string"
              },
              "website": {
                "type": "string"
              },
              "youtube": {
                "type": "string"
              }
            }
          },
          "public": {
            "type": "boolean"
          }
        },
        "required": [
          "id"
        ]
      },
      "Tiinyverse": {
        "type": "object",
        "properties": {
          "profileUrl": {
            "type": "string",
            "format": "uri"
          },
          "name": {
            "type": "string"
          },
          "verifiedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "profileUrl",
          "name"
        ]
      },
      "Token": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "The first 13 characters, for display."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "createdAt",
          "lastUsedAt"
        ]
      },
      "Comment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "handle": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "text": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "canDelete": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "author",
          "text",
          "at",
          "canDelete"
        ]
      },
      "Social": {
        "type": "object",
        "properties": {
          "seeds": {
            "type": "integer",
            "description": "How many people have given this app a seed."
          },
          "thumbs": {
            "type": "integer",
            "description": "The same number under the name the first release used. Read seeds."
          },
          "mine": {
            "type": "boolean"
          },
          "comments": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Comment"
            }
          },
          "page": {
            "type": "integer",
            "minimum": 0
          },
          "nextPage": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          }
        },
        "required": [
          "seeds",
          "thumbs",
          "mine",
          "comments",
          "page",
          "nextPage"
        ]
      },
      "SocialCounts": {
        "type": "object",
        "properties": {
          "apps": {
            "type": "object",
            "description": "Keyed by app id.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "seeds": {
                  "type": "integer"
                },
                "comments": {
                  "type": "integer"
                }
              },
              "required": [
                "seeds",
                "comments"
              ]
            }
          },
          "makers": {
            "type": "object",
            "description": "Keyed by maker handle.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "seeds": {
                  "type": "integer"
                }
              },
              "required": [
                "seeds"
              ]
            }
          }
        },
        "required": [
          "apps",
          "makers"
        ]
      },
      "Seed": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
          },
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "preparing",
              "label pending",
              "awaiting review",
              "draft",
              "closed",
              "merged",
              "published",
              "sprouting",
              "submission failed",
              "submission uncertain"
            ]
          },
          "prUrl": {
            "type": "string",
            "format": "uri"
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          },
          "reviews": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "seeds": {
            "type": "integer"
          },
          "thumbs": {
            "type": "integer",
            "description": "The same number as seeds."
          },
          "comments": {
            "type": "integer"
          },
          "url": {
            "type": "string"
          },
          "canUpdate": {
            "type": "boolean"
          },
          "release": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ReleaseCheck"
              },
              {
                "type": "null"
              }
            ]
          },
          "unavailable": {
            "type": "boolean",
            "description": "GitHub could not be read, so state is the stored one."
          }
        },
        "required": [
          "id",
          "name",
          "version",
          "state",
          "checks",
          "reviews"
        ]
      },
      "ReleaseCheck": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "found",
              "listed",
              "none",
              "manual",
              "untracked"
            ]
          },
          "message": {
            "type": "string",
            "description": "The sentence shown to the maker."
          },
          "checkedAt": {
            "type": "integer",
            "description": "Milliseconds since the epoch."
          },
          "version": {
            "type": "string"
          },
          "prUrl": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "status",
          "message",
          "checkedAt",
          "version"
        ]
      },
      "Art": {
        "type": "object",
        "properties": {
          "scene": {
            "type": "string"
          },
          "header": {
            "type": "string",
            "format": "uri"
          },
          "icon": {
            "type": "string",
            "format": "uri"
          },
          "remaining": {
            "type": "integer",
            "description": "Drawings left for this app today."
          }
        },
        "required": [
          "scene",
          "remaining"
        ]
      },
      "Manifest": {
        "type": "object",
        "description": "One catalog entry. Every field is documented at https://tiinyapp.farm/docs/manifest/ and the machine-readable schema is at https://tiinyapp.farm/docs/manifest.schema.json."
      }
    }
  }
}
