{
  "openapi": "3.1.0",
  "info": {
    "title": "Evergist API",
    "version": "1.0.0",
    "description": "Store and fetch end-to-end encrypted, expiring text. Clients encrypt with the v1 scheme (AES-256-GCM, HKDF-SHA256, optional PBKDF2) before calling this API; the server only accepts ciphertext. Scheme: https://evergist.com/security/#the-encryption-scheme. Clients that do the crypto for you: https://evergist.com/agents/",
    "contact": {
      "email": "contact@evergist.com",
      "url": "https://evergist.com/"
    }
  },
  "externalDocs": {
    "url": "https://evergist.com/docs/api/"
  },
  "servers": [
    {
      "url": "https://evergist.com/api/v1"
    }
  ],
  "components": {
    "securitySchemes": {
      "accessToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "base64url HKDF(K, 'evergist/v1/access'). For reads of password gists: 'access.proof'. For deletes: a read token or the delete token."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "attemptsRemaining": {
            "type": "integer"
          }
        }
      },
      "CreateGist": {
        "type": "object",
        "required": [
          "v",
          "iv",
          "ciphertext",
          "accessHash",
          "password"
        ],
        "properties": {
          "v": {
            "const": 1
          },
          "iv": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "minLength": 16,
            "maxLength": 16,
            "description": "12-byte AES-GCM IV, base64url."
          },
          "ciphertext": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "AES-256-GCM ciphertext with tag, base64url. At most 524,304 bytes decoded."
          },
          "accessHash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Hex SHA-256 of the access token."
          },
          "password": {
            "type": "boolean"
          },
          "proofHash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Hex SHA-256 of the password proof. Required iff password is true."
          },
          "iterations": {
            "type": "integer",
            "minimum": 100000,
            "maximum": 10000000,
            "description": "PBKDF2 iterations. Required iff password is true. Clients use 600000."
          },
          "expiresIn": {
            "type": "integer",
            "minimum": 60,
            "maximum": 2592000,
            "default": 86400
          },
          "maxViews": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 1000,
            "default": null
          }
        }
      },
      "CreatedGist": {
        "type": "object",
        "required": [
          "id",
          "url",
          "expiresAt",
          "maxViews",
          "deleteToken"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Append '#' + base64url link key to build the share link."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "maxViews": {
            "type": [
              "integer",
              "null"
            ]
          },
          "deleteToken": {
            "type": "string",
            "description": "Deletes the gist. Keep private."
          }
        }
      },
      "GistStatus": {
        "type": "object",
        "required": [
          "id",
          "password",
          "expiresAt",
          "maxViews",
          "viewsRemaining"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "password": {
            "type": "boolean",
            "description": "Whether a password is mixed into the key."
          },
          "iterations": {
            "type": "integer",
            "description": "PBKDF2 iterations, present when password is true."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "maxViews": {
            "type": [
              "integer",
              "null"
            ]
          },
          "viewsRemaining": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "SealedGist": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "password",
              "expiresAt",
              "maxViews",
              "viewsRemaining"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "password": {
                "type": "boolean",
                "description": "Whether a password is mixed into the key."
              },
              "iterations": {
                "type": "integer",
                "description": "PBKDF2 iterations, present when password is true."
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time"
              },
              "maxViews": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "viewsRemaining": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "v",
              "iv",
              "ciphertext",
              "burned"
            ],
            "properties": {
              "v": {
                "const": 1
              },
              "iv": {
                "type": "string",
                "pattern": "^[A-Za-z0-9_-]+$"
              },
              "ciphertext": {
                "type": "string",
                "pattern": "^[A-Za-z0-9_-]+$"
              },
              "burned": {
                "type": "boolean",
                "description": "True if this read used the last view and the gist is now deleted."
              }
            }
          }
        ]
      }
    }
  },
  "paths": {
    "/gists": {
      "post": {
        "operationId": "createGist",
        "summary": "Store an encrypted gist",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGist"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedGist"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type must be application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gists/{id}/status": {
      "get": {
        "operationId": "getGistStatus",
        "summary": "Expiry, views left, and password flag. Does not use a view.",
        "security": [
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9A-HJ-NP-Za-km-z]{12}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GistStatus"
                }
              }
            }
          },
          "404": {
            "description": "Not found, expired, or wrong access token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gists/{id}": {
      "get": {
        "operationId": "readGist",
        "summary": "Fetch the ciphertext. Uses one view.",
        "security": [
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9A-HJ-NP-Za-km-z]{12}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ciphertext",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SealedGist"
                }
              }
            }
          },
          "401": {
            "description": "Wrong password proof; includes attemptsRemaining",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, expired, or wrong access token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteGist",
        "summary": "Delete now",
        "security": [
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9A-HJ-NP-Za-km-z]{12}$"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Wrong password proof",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/stats": {
      "get": {
        "operationId": "getStats",
        "summary": "Public daily counters",
        "responses": {
          "200": {
            "description": "Totals and last 30 days"
          }
        }
      }
    }
  }
}
