{
  "openapi": "3.1.0",
  "info": {
    "title": "Secure Chat App API",
    "version": "0.3.0",
    "description": "Zero-knowledge End-to-End Encrypted (E2EE) chat server with client-signed JWT authentication, WebCrypto public key registry, TOTP out-of-band chat joining, and epoch-millis message synchronization."
  },
  "servers": [
    {
      "url": "/",
      "description": "Current Server (Local or Cloud Run)"
    }
  ],
  "components": {
    "securitySchemes": {
      "ClientSignedJWT": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "ES256 JWT signed using the client's WebCrypto private key. The payload must include `sub` set to the caller's session UUID."
      }
    },
    "schemas": {
      "PublicKeyData": {
        "type": "object",
        "properties": {
          "spki": {
            "type": "string",
            "description": "SubjectPublicKeyInfo formatted Base64 string"
          },
          "jwk": {
            "type": "object",
            "description": "JSON Web Key format representation"
          }
        }
      },
      "PublicKeysRegistry": {
        "type": "object",
        "required": ["signing", "encryption"],
        "properties": {
          "signing": {
            "$ref": "#/components/schemas/PublicKeyData",
            "description": "ECDSA P-256 public key used to verify client-signed JWTs"
          },
          "encryption": {
            "$ref": "#/components/schemas/PublicKeyData",
            "description": "ECDH P-256 public key used by peers for E2EE key encapsulation"
          }
        }
      },
      "RegisterKeyRequest": {
        "type": "object",
        "required": ["id", "publicKeys"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Persistent client session UUID"
          },
          "name": {
            "type": "string",
            "description": "Unrestricted display name (supports any characters, emojis, symbols, or empty)"
          },
          "publicKeys": {
            "$ref": "#/components/schemas/PublicKeysRegistry"
          }
        }
      },
      "SessionKeyRecord": {
        "type": "object",
        "required": ["id", "name", "publicKeys", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "publicKeys": { "$ref": "#/components/schemas/PublicKeysRegistry" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "UpdateNameRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Updated display name without restrictions"
          }
        }
      },
      "CreateChatRequest": {
        "type": "object",
        "required": ["title"],
        "properties": {
          "title": {
            "type": "string",
            "example": "Security Team Sync"
          }
        }
      },
      "ChatRecord": {
        "type": "object",
        "required": ["id", "title", "creatorId", "members", "createdAt"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-zA-Z]{5}$",
            "example": "k8X9p",
            "description": "5-character short ID used for joining"
          },
          "title": { "type": "string" },
          "creatorId": { "type": "string", "format": "uuid" },
          "members": {
            "type": "array",
            "items": { "type": "string", "format": "uuid" }
          },
          "currentTotp": {
            "type": "string",
            "pattern": "^[0-9]{6}$",
            "description": "Active 6-digit TOTP token valid in current 30s epoch (only visible to enrolled members)"
          },
          "expiresInSeconds": {
            "type": "integer",
            "description": "Seconds remaining before token rotation"
          },
          "inviteKey": {
            "type": "string",
            "example": "k8X9p/482019",
            "description": "Combined out-of-band invite token"
          },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "JoinChatRequest": {
        "type": "object",
        "required": ["token"],
        "properties": {
          "token": {
            "type": "string",
            "pattern": "^[0-9]{6}$",
            "example": "482019",
            "description": "Current 6-digit TOTP token"
          }
        }
      },
      "EncryptedMessagePayload": {
        "type": "object",
        "required": ["id", "timestamp", "senderId", "iv", "ciphertext", "ephemeralPublicKey", "keys"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "timestamp": {
            "type": "integer",
            "format": "int64",
            "description": "Monotonic server epoch milliseconds"
          },
          "senderId": { "type": "string", "format": "uuid" },
          "senderName": { "type": "string" },
          "iv": {
            "type": "string",
            "description": "Base64 encoded 12-byte initialization vector"
          },
          "ciphertext": {
            "type": "string",
            "description": "Base64 encoded AES-256-GCM ciphertext"
          },
          "ephemeralPublicKey": {
            "type": "string",
            "description": "Base64 SPKI encoded ephemeral ECDH public key from sender"
          },
          "keys": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Map of member UUID to wrapped AES content key"
          }
        }
      },
      "SendMessageRequest": {
        "type": "object",
        "required": ["iv", "ciphertext", "ephemeralPublicKey", "keys"],
        "properties": {
          "iv": { "type": "string" },
          "ciphertext": { "type": "string" },
          "ephemeralPublicKey": { "type": "string" },
          "keys": {
            "type": "object",
            "additionalProperties": { "type": "string" }
          }
        }
      },
      "AcknowledgeRequest": {
        "type": "object",
        "required": ["lastSeenMillis"],
        "properties": {
          "lastSeenMillis": {
            "type": "integer",
            "format": "int64",
            "description": "Cumulative epoch millisecond timestamp up to which all messages have been read"
          }
        }
      }
    }
  },
  "paths": {
    "/healthz": {
      "get": {
        "summary": "Health check probe",
        "responses": {
          "200": {
            "description": "Service healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "healthy" },
                    "time": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/info": {
      "get": {
        "summary": "Server & Cloud Run information",
        "responses": {
          "200": {
            "description": "Environment and runtime information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": { "type": "string" },
                    "revision": { "type": "string" },
                    "region": { "type": "string", "example": "europe-west4" },
                    "gcs_bucket": { "type": "string" },
                    "gcs_active": { "type": "boolean" },
                    "runtime": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys": {
      "post": {
        "summary": "Register public keys and session",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RegisterKeyRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session registered successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SessionKeyRecord" }
              }
            }
          }
        }
      },
      "get": {
        "summary": "List all registered public keys",
        "responses": {
          "200": {
            "description": "List of all user public keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/SessionKeyRecord" }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys/{id}": {
      "get": {
        "summary": "Retrieve user public key and display name",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "User public key record",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SessionKeyRecord" }
              }
            }
          },
          "404": { "description": "Session not found" }
        }
      }
    },
    "/api/v1/keys/{id}/name": {
      "patch": {
        "summary": "Update session display name (no restrictions)",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateNameRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Name updated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SessionKeyRecord" }
              }
            }
          },
          "401": { "description": "Unauthorized signature" }
        }
      }
    },
    "/api/v1/chats": {
      "post": {
        "summary": "Create new chat room with 5-character short ID",
        "security": [{ "ClientSignedJWT": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateChatRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Chat created",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatRecord" }
              }
            }
          }
        }
      },
      "get": {
        "summary": "List chats the authenticated user belongs to",
        "security": [{ "ClientSignedJWT": [] }],
        "responses": {
          "200": {
            "description": "Enrolled chats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/ChatRecord" }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chats/{chatId}": {
      "get": {
        "summary": "Get chat metadata and active members",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[0-9a-zA-Z]{5}$" } }
        ],
        "responses": {
          "200": {
            "description": "Chat details",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatRecord" }
              }
            }
          },
          "403": { "description": "Not a member of this chat" }
        }
      },
      "delete": {
        "summary": "Nuke chat permanently (creator only)",
        "description": "Permanently deletes the chat record and all messages from GCS and server memory, broadcasting a `chat_nuked` SSE event to all connected members.",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[0-9a-zA-Z]{5}$" } }
        ],
        "responses": {
          "200": {
            "description": "Chat nuked and obliterated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "nuked" },
                    "chatId": { "type": "string", "example": "k8X9p" }
                  }
                }
              }
            }
          },
          "403": { "description": "Only the creator can nuke this chat" },
          "404": { "description": "Chat not found" }
        }
      }
    },
    "/api/v1/chats/{chatId}/join": {
      "post": {
        "summary": "Join chat using out-of-band 30s TOTP token",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[0-9a-zA-Z]{5}$" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JoinChatRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined chat room successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatRecord" }
              }
            }
          },
          "403": { "description": "Invalid or expired TOTP token (30s window)" }
        }
      }
    },
    "/api/v1/chats/{chatId}/stream": {
      "get": {
        "summary": "Server-Sent Events (SSE) stream for real-time TOTP, messages, and presence",
        "description": "Streams event types: `totp`, `message`, `member_joined`, `chat_nuked`, `messages_purged`, `ping`.",
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "token", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Client signed JWT" },
          { "name": "since", "in": "query", "required": false, "schema": { "type": "integer", "format": "int64" }, "description": "Replay missed messages since epoch millis" }
        ],
        "responses": {
          "200": {
            "description": "Event stream connection",
            "content": {
              "text/event-stream": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/api/v1/chats/{chatId}/messages": {
      "post": {
        "summary": "Send an End-to-End Encrypted message",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendMessageRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message relayed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EncryptedMessagePayload" }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Catch-up missed unread messages",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "since", "in": "query", "required": false, "schema": { "type": "integer", "format": "int64" } }
        ],
        "responses": {
          "200": {
            "description": "List of encrypted message payloads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/EncryptedMessagePayload" }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chats/{chatId}/ack": {
      "post": {
        "summary": "Acknowledge read messages via epoch millis cursor",
        "description": "When all chat members have acknowledged messages up to timestamp T, server purges all messages <= T.",
        "security": [{ "ClientSignedJWT": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AcknowledgeRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged; server updated min cutoff",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "acknowledged" },
                    "purgedUpTo": { "type": "integer", "format": "int64" }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
