{
  "openapi": "3.1.0",
  "info": {
    "title": "Kasaneru Public API",
    "version": "0.1.0",
    "description": "Code-first OpenAPI document for the Kasaneru public HTTP API."
  },
  "servers": [
    {
      "url": "https://api.kasaneru.nekonata.dev",
      "description": "Public production API endpoint"
    },
    {
      "url": "http://127.0.0.1:5001/{projectId}/asia-northeast1/api",
      "description": "Firebase Emulator",
      "variables": {
        "projectId": {
          "default": "kasaneru-48df5"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "api-keys",
      "description": "PAT management endpoints"
    },
    {
      "name": "availability",
      "description": "Availability query endpoints"
    }
  ],
  "components": {
    "securitySchemes": {
      "FirebaseIdToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Firebase ID token for PAT management endpoints."
      },
      "KasaneruPat": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "PAT",
        "description": "Kasaneru personal access token for public query endpoints."
      }
    },
    "schemas": {
      "CreateApiKeyResponse": {
        "type": "object",
        "properties": {
          "keyId": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "availability:read"
              ]
            }
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "rateLimit": {
            "type": "integer"
          },
          "warning": {
            "type": "string"
          }
        },
        "required": [
          "keyId",
          "token",
          "name",
          "scopes",
          "expiresAt",
          "rateLimit"
        ]
      },
      "ApiErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "CreateApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "expiresDays": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 365
              },
              {
                "type": "null"
              }
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "availability:read"
              ]
            },
            "default": [
              "availability:read"
            ]
          },
          "rateLimit": {
            "type": "integer",
            "minimum": 10,
            "maximum": 600,
            "default": 60
          }
        },
        "required": [
          "name"
        ]
      },
      "ListApiKeysResponse": {
        "type": "object",
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeySummary"
            }
          }
        },
        "required": [
          "keys"
        ]
      },
      "ApiKeySummary": {
        "type": "object",
        "properties": {
          "keyId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "availability:read"
              ]
            }
          },
          "keyPrefix": {
            "type": "string"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "rateLimit": {
            "type": "integer"
          },
          "revoked": {
            "type": "boolean"
          }
        },
        "required": [
          "keyId",
          "name",
          "scopes",
          "keyPrefix",
          "expiresAt",
          "revokedAt",
          "lastUsedAt",
          "createdAt",
          "rateLimit",
          "revoked"
        ]
      },
      "RevokeApiKeyResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "success"
        ]
      },
      "AvailabilityQueryResponse": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "group",
              "me"
            ]
          },
          "groupId": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "requiredCount": {
            "type": "integer"
          },
          "allRequired": {
            "type": "boolean"
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CandidateSlot"
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "interval": {
                "type": "integer"
              },
              "duration": {
                "type": "integer"
              },
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "end": {
                "type": "string",
                "format": "date-time"
              },
              "generatedAt": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "interval",
              "duration",
              "start",
              "end",
              "generatedAt"
            ]
          }
        },
        "required": [
          "mode",
          "timezone",
          "requiredCount",
          "allRequired",
          "slots",
          "meta"
        ]
      },
      "CandidateSlot": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "availableCount": {
            "type": "integer"
          },
          "requiredCount": {
            "type": "integer"
          },
          "score": {
            "type": "number"
          }
        },
        "required": [
          "start",
          "end",
          "availableCount",
          "requiredCount",
          "score"
        ]
      },
      "AvailabilityQueryMeRequest": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "type": "string",
            "minLength": 1
          },
          "duration": {
            "type": "integer",
            "minimum": 15,
            "maximum": 720
          },
          "interval": {
            "type": "integer",
            "minimum": 5,
            "maximum": 120,
            "default": 30
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20
          }
        },
        "required": [
          "start",
          "end",
          "timezone",
          "duration"
        ]
      },
      "AvailabilityQueryGroupRequest": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "type": "string",
            "minLength": 1
          },
          "duration": {
            "type": "integer",
            "minimum": 15,
            "maximum": 720
          },
          "interval": {
            "type": "integer",
            "minimum": 5,
            "maximum": 120,
            "default": 30
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20
          },
          "groupId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "start",
          "end",
          "timezone",
          "duration",
          "groupId"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/v1/api-keys": {
      "post": {
        "tags": [
          "api-keys"
        ],
        "summary": "Create a personal access token",
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateApiKeyResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "api-keys"
        ],
        "summary": "List personal access tokens",
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current user's API keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListApiKeysResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "api-keys"
        ],
        "summary": "Revoke a personal access token",
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Revocation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeApiKeyResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Token is not owned by the current user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "API key not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/availability/query/me": {
      "post": {
        "tags": [
          "availability"
        ],
        "summary": "Query personal availability",
        "security": [
          {
            "KasaneruPat": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AvailabilityQueryMeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability candidates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityQueryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired, or revoked PAT",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Permission denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Group not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/availability/query/group": {
      "post": {
        "tags": [
          "availability"
        ],
        "summary": "Query group availability",
        "security": [
          {
            "KasaneruPat": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AvailabilityQueryGroupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability candidates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityQueryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired, or revoked PAT",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Permission denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Group not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
