flora is a fast and secure runtime that lets you write discord bots for your servers, with a rich TypeScript SDK, without worrying about running infrastructure. [mirror]
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(runtime/api): rework openapi defs

+170 -49
+41
AGENTS.md
··· 10 10 - At the end of each plan, give me a list of unresolved questions to answer, if any. Make the questions extremely concise. 11 11 - Prefer runtime workflows via `./x` commands. 12 12 13 + ## OpenAPI Writing + Naming 14 + 15 + ### Model Names 16 + 17 + - Singular nouns, PascalCase (`Invoice`, `LineItem`, `PaymentMethod`). 18 + - Name by what data represents, not how used (`Address` not `UserAddressInputObject`). 19 + - Use one variant suffix convention consistently: 20 + - `CreateUserRequest`, `UpdateUserRequest`, `UserResponse` (preferred). 21 + - Avoid redundant prefixes within same service (`Profile`, `Settings`, `Preferences`). 22 + 23 + ### Local Overrides 24 + 25 + - `CreateXRequest` naming is preferred. 26 + - `snake_case` is OK in OpenAPI schemas. 27 + - Non-statement booleans are OK. 28 + - Use "Guild ID" wording in params/descriptions. 29 + - Tag `deployment` only if it represents a singular resource; otherwise use plural. 30 + 31 + ### Property Names 32 + 33 + - Domain concept names, not implementation (`expiresAt` not `exp_timestamp_unix`). 34 + - Booleans read as statements (`isActive`, `hasVerifiedEmail`, `requiresMfa`). 35 + - Dates/times always have a suffix (`createdAt`, `scheduledFor`, `cancelledAt`). 36 + 37 + ### Descriptions 38 + 39 + - Schema descriptions answer “what is this?”. 40 + - Property descriptions answer “what does this mean and what are bounds?”. 41 + - Explicitly state units, null meaning, mutability, constraints, enum meaning. 42 + 43 + ### Operation Descriptions 44 + 45 + - `summary`: short verb phrase. 46 + - `description`: side effects, preconditions, conflict behavior. 47 + 48 + ### General Writing 49 + 50 + - Write for new devs. 51 + - Use present tense (“Returns”, “Creates”). 52 + - Avoid filler. Keep terminology consistent. 53 + 13 54 ## Runtime Commands (`./x`) 14 55 15 56 Use these from repo root:
+4 -4
apps/runtime/src/handlers/auth.rs
··· 26 26 #[openapi( 27 27 paths(login_handler, callback_handler, me_handler), 28 28 components(schemas(AuthUser, AuthResponse, crate::handlers::error::ErrorResponse)), 29 - tags((name = "auth", description = "Discord authentication")) 29 + tags((name = "Auth", description = "Discord authentication")) 30 30 )] 31 31 pub struct AuthApi; 32 32 ··· 73 73 #[utoipa::path( 74 74 get, 75 75 path = "/login", 76 - tag = "auth", 76 + tag = "Auth", 77 77 responses( 78 78 (status = 302, description = "Redirect to Discord") 79 79 ) ··· 98 98 #[utoipa::path( 99 99 get, 100 100 path = "/callback", 101 - tag = "auth", 101 + tag = "Auth", 102 102 params( 103 103 ("code" = String, Query, description = "Discord authorization code"), 104 104 ("state" = String, Query, description = "Opaque state value returned by Discord") ··· 171 171 #[utoipa::path( 172 172 get, 173 173 path = "/me", 174 - tag = "auth", 174 + tag = "Auth", 175 175 responses( 176 176 (status = 200, description = "Session is valid", body = AuthResponse), 177 177 (status = 401, description = "No active session", body = crate::handlers::error::ErrorResponse)
+23 -3
apps/runtime/src/handlers/builds.rs
··· 23 23 #[openapi( 24 24 paths(create_build_handler, get_build_handler), 25 25 components(schemas(CreateBuildResponse, BuildStatusResponse)), 26 - tags((name = "builds", description = "Server-side build pipeline")) 26 + tags((name = "Builds", description = "Server-side build pipeline")) 27 27 )] 28 28 pub struct BuildApi; 29 29 ··· 34 34 .route("/{build_id}/logs", get(stream_build_logs_handler)) 35 35 } 36 36 37 + /// Response returned when a build is created. 37 38 #[derive(Debug, Serialize, Deserialize, ToSchema)] 38 39 pub struct CreateBuildResponse { 40 + /// Build identifier. 39 41 pub build_id: String, 42 + /// Current build status string. 40 43 pub status: String, 41 44 } 42 45 46 + /// Build artifact output paths. 43 47 #[derive(Debug, Serialize, Deserialize, ToSchema)] 44 48 pub struct BuildArtifactResponse { 49 + /// Bundled JavaScript output. 45 50 pub bundle: String, 51 + /// Source map for the bundle. 46 52 pub source_map: String, 47 53 } 48 54 55 + /// Current build status and output. 49 56 #[derive(Debug, Serialize, Deserialize, ToSchema)] 50 57 pub struct BuildStatusResponse { 58 + /// Build identifier. 51 59 pub build_id: String, 60 + /// Guild ID the build targets. 52 61 pub guild_id: String, 62 + /// Entry file path used for the build. 53 63 pub entry: String, 64 + /// Current build status string. 54 65 pub status: String, 66 + /// Build logs, newest last. 55 67 pub logs: Vec<String>, 68 + /// Build start time in RFC3339 (UTC). 56 69 #[serde(skip_serializing_if = "Option::is_none")] 57 70 pub started_at: Option<String>, 71 + /// Build completion time in RFC3339 (UTC). 58 72 #[serde(skip_serializing_if = "Option::is_none")] 59 73 pub finished_at: Option<String>, 74 + /// Generated build artifacts, if available. 60 75 #[serde(skip_serializing_if = "Option::is_none")] 61 76 pub artifact: Option<BuildArtifactResponse>, 77 + /// Error detail when the build fails. 62 78 #[serde(skip_serializing_if = "Option::is_none")] 63 79 pub error: Option<String>, 64 80 } ··· 66 82 #[utoipa::path( 67 83 post, 68 84 path = "/", 69 - tag = "builds", 85 + tag = "Builds", 86 + summary = "Create a build", 87 + description = "Queues a server-side build for the provided project archive.", 70 88 responses( 71 89 (status = 200, description = "Build queued", body = CreateBuildResponse), 72 90 (status = 400, description = "Invalid request", body = crate::handlers::error::ErrorResponse), ··· 146 164 #[utoipa::path( 147 165 get, 148 166 path = "/{build_id}", 149 - tag = "builds", 167 + tag = "Builds", 168 + summary = "Get build status", 169 + description = "Returns the current status, logs, and artifacts for a build.", 150 170 params( 151 171 ("build_id" = String, Path, description = "Build ID") 152 172 ),
+4 -2
apps/runtime/src/handlers/deployments/history.rs
··· 37 37 get, 38 38 path = "/{guild_id}/history", 39 39 params( 40 - ("guild_id" = String, Path, description = "Discord guild id"), 40 + ("guild_id" = String, Path, description = "Guild ID"), 41 41 ("limit" = Option<i64>, Query, description = "Page size, max 100"), 42 42 ("cursor_deployed_at" = Option<String>, Query, description = "RFC3339 deployed_at cursor"), 43 43 ("cursor_id" = Option<String>, Query, description = "Revision id cursor"), 44 44 ("include_bundle" = Option<bool>, Query, description = "Include bundled output in response") 45 45 ), 46 - tag = "deployment", 46 + tag = "Deployments", 47 + summary = "List deployment history", 48 + description = "Returns deployment revisions for a guild in reverse chronological order.", 47 49 responses( 48 50 (status = 200, description = "Revision history", body = [DeploymentRevisionResponse]), 49 51 (status = 400, description = "Invalid cursor", body = crate::handlers::error::ErrorResponse),
+3 -1
apps/runtime/src/handlers/deployments/list.rs
··· 15 15 #[utoipa::path( 16 16 get, 17 17 path = "/", 18 - tag = "deployment", 18 + tag = "Deployments", 19 + summary = "List deployments", 20 + description = "Returns deployment snapshots the caller can administer.", 19 21 responses( 20 22 (status = 200, description = "Deployments retrieved", body = [DeploymentResponse]), 21 23 (status = 500, description = "Internal server error", body = crate::handlers::error::ErrorResponse)
+1 -1
apps/runtime/src/handlers/deployments/mod.rs
··· 39 39 super::error::ErrorResponse 40 40 ) 41 41 ), 42 - tags((name = "deployment", description = "Manage per-guild bot deployments")) 42 + tags((name = "Deployments", description = "Manage per-guild bot deployments")) 43 43 )] 44 44 pub struct DeploymentApi; 45 45
+4 -2
apps/runtime/src/handlers/deployments/read.rs
··· 26 26 get, 27 27 path = "/{guild_id}", 28 28 params( 29 - ("guild_id" = String, Path, description = "Discord guild id"), 29 + ("guild_id" = String, Path, description = "Guild ID"), 30 30 ("include_bundle" = Option<bool>, Query, description = "Include bundled output in response") 31 31 ), 32 - tag = "deployment", 32 + tag = "Deployments", 33 + summary = "Get deployment", 34 + description = "Returns the latest successful deployment snapshot for a guild.", 33 35 responses( 34 36 (status = 200, description = "Deployment found", body = DeploymentResponse), 35 37 (status = 404, description = "Deployment not found", body = crate::handlers::error::ErrorResponse),
+4 -2
apps/runtime/src/handlers/deployments/revision.rs
··· 27 27 get, 28 28 path = "/{guild_id}/revisions/{revision_id}", 29 29 params( 30 - ("guild_id" = String, Path, description = "Discord guild id"), 30 + ("guild_id" = String, Path, description = "Guild ID"), 31 31 ("revision_id" = String, Path, description = "Revision id"), 32 32 ("include_bundle" = Option<bool>, Query, description = "Include bundled output in response") 33 33 ), 34 - tag = "deployment", 34 + tag = "Deployments", 35 + summary = "Get deployment revision", 36 + description = "Returns details for a specific deployment revision.", 35 37 responses( 36 38 (status = 200, description = "Revision found", body = DeploymentRevisionResponse), 37 39 (status = 404, description = "Revision not found", body = crate::handlers::error::ErrorResponse),
+4 -2
apps/runtime/src/handlers/deployments/rollback.rs
··· 26 26 post, 27 27 path = "/{guild_id}/rollback/{revision_id}", 28 28 params( 29 - ("guild_id" = String, Path, description = "Discord guild id"), 29 + ("guild_id" = String, Path, description = "Guild ID"), 30 30 ("revision_id" = String, Path, description = "Successful revision id to rollback to") 31 31 ), 32 - tag = "deployment", 32 + tag = "Deployments", 33 + summary = "Rollback deployment", 34 + description = "Deploys a previous successful revision and records a new rollback revision.", 33 35 responses( 34 36 (status = 200, description = "Rollback created", body = DeploymentRevisionResponse), 35 37 (status = 404, description = "Revision not found", body = crate::handlers::error::ErrorResponse),
+13 -2
apps/runtime/src/handlers/deployments/upsert.rs
··· 38 38 pub build_id: Option<String>, 39 39 } 40 40 41 + /// Deployment snapshot stored for a guild. 41 42 #[derive(Debug, Deserialize, Serialize, ToSchema)] 42 43 pub struct DeploymentResponse { 44 + /// Guild ID for the deployment. 43 45 pub guild_id: String, 46 + /// Snapshot creation time in RFC3339 (UTC). 44 47 pub created_at: String, 48 + /// Snapshot update time in RFC3339 (UTC). 45 49 pub updated_at: String, 50 + /// Entry file path used for the deployment. 46 51 pub entry: String, 52 + /// Raw files when the deployment was uploaded as sources. 47 53 #[serde(skip_serializing_if = "Option::is_none")] 48 54 pub files: Option<Vec<DeploymentFile>>, 55 + /// Source map included with the bundle, if provided. 49 56 #[serde(skip_serializing_if = "Option::is_none")] 50 57 pub source_map: Option<DeploymentSourceMapFile>, 58 + /// Bundled output when stored, if included. 51 59 #[serde(skip_serializing_if = "Option::is_none")] 52 60 pub bundle: Option<String>, 53 61 } ··· 67 75 pub guild_id: String, 68 76 pub entry: String, 69 77 pub status: DeploymentRevisionStatus, 78 + /// Deployment time in RFC3339 (UTC). 70 79 pub deployed_at: String, 71 80 pub deploy_source: DeploymentSource, 72 81 pub actor: DeploymentActorResponse, ··· 244 253 path = "/{guild_id}", 245 254 request_body = DeploymentRequest, 246 255 params( 247 - ("guild_id" = String, Path, description = "Discord guild id") 256 + ("guild_id" = String, Path, description = "Guild ID") 248 257 ), 249 - tag = "deployment", 258 + tag = "Deployments", 259 + summary = "Deploy a guild", 260 + description = "Creates or updates the active deployment for a guild, then records a revision.", 250 261 responses( 251 262 (status = 200, description = "Deployment stored", body = DeploymentResponse), 252 263 (status = 500, description = "Internal server error", body = crate::handlers::error::ErrorResponse)
+2 -2
apps/runtime/src/handlers/guilds.rs
··· 16 16 #[openapi( 17 17 paths(list_guilds_handler), 18 18 components(schemas(GuildResponse, crate::handlers::error::ErrorResponse)), 19 - tags((name = "guilds", description = "Guilds the user can manage")) 19 + tags((name = "Guilds", description = "Guilds the user can manage")) 20 20 )] 21 21 pub struct GuildApi; 22 22 ··· 37 37 #[utoipa::path( 38 38 get, 39 39 path = "/", 40 - tag = "guilds", 40 + tag = "Guilds", 41 41 responses( 42 42 (status = 200, description = "Guilds available for deployment", body = [GuildResponse]), 43 43 (status = 401, description = "Not authenticated", body = crate::handlers::error::ErrorResponse)
+2 -2
apps/runtime/src/handlers/health.rs
··· 2 2 #[derive(utoipa::OpenApi)] 3 3 #[openapi( 4 4 paths(health_check), 5 - tags((name = "health", description = "Health endpoints")) 5 + tags((name = "Health", description = "Health endpoints")) 6 6 )] 7 7 pub struct HealthApi; 8 8 ··· 10 10 #[utoipa::path( 11 11 get, 12 12 path = "/", 13 - tag = "health", 13 + tag = "Health", 14 14 responses( 15 15 (status = 200, description = "API is healthy", body = String) 16 16 )
+3 -1
apps/runtime/src/handlers/kv/create_store.rs
··· 26 26 #[utoipa::path( 27 27 post, 28 28 path = "/stores", 29 + summary = "Create a store", 30 + description = "Creates a new key-value store for the specified guild.", 29 31 request_body = CreateStoreRequest, 30 32 responses( 31 33 (status = 200, description = "Store created successfully", body = CreateStoreResponse), ··· 34 36 (status = 403, description = "Not guild admin"), 35 37 (status = 500, description = "Internal server error"), 36 38 ), 37 - tag = "kv" 39 + tag = "KV" 38 40 )] 39 41 pub async fn create_store_handler( 40 42 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/delete_key.rs
··· 29 29 ("store_name" = String, Path, description = "Store name"), 30 30 ("key" = String, Path, description = "Key to delete"), 31 31 ), 32 + summary = "Delete a key", 33 + description = "Deletes a key from the store if it exists.", 32 34 responses( 33 35 (status = 200, description = "Key deleted successfully"), 34 36 (status = 401, description = "Not authenticated"), ··· 36 38 (status = 404, description = "Store not found"), 37 39 (status = 500, description = "Internal server error"), 38 40 ), 39 - tag = "kv" 41 + tag = "KV" 40 42 )] 41 43 pub async fn delete_key_handler( 42 44 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/delete_store.rs
··· 27 27 ("guild_id" = String, Path, description = "Guild ID"), 28 28 ("store_name" = String, Path, description = "Store name"), 29 29 ), 30 + summary = "Delete a store", 31 + description = "Deletes a store and all stored keys.", 30 32 responses( 31 33 (status = 200, description = "Store deleted successfully"), 32 34 (status = 401, description = "Not authenticated"), ··· 34 36 (status = 404, description = "Store not found"), 35 37 (status = 500, description = "Internal server error"), 36 38 ), 37 - tag = "kv" 39 + tag = "KV" 38 40 )] 39 41 pub async fn delete_store_handler( 40 42 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/export.rs
··· 35 35 params( 36 36 ("guild_id" = String, Path, description = "Guild ID"), 37 37 ), 38 + summary = "Export guild stores", 39 + description = "Creates a backup of all stores for a guild and returns a backup id.", 38 40 responses( 39 41 (status = 200, description = "Export created successfully", body = ExportGuildResponse), 40 42 (status = 401, description = "Not authenticated"), ··· 42 44 (status = 404, description = "No stores found for guild"), 43 45 (status = 500, description = "Internal server error"), 44 46 ), 45 - tag = "kv" 47 + tag = "KV" 46 48 )] 47 49 pub async fn export_guild_handler( 48 50 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/get.rs
··· 36 36 ("store_name" = String, Path, description = "Store name"), 37 37 ("key" = String, Path, description = "Key to retrieve"), 38 38 ), 39 + summary = "Get a value", 40 + description = "Returns the stored value or null when the key is missing.", 39 41 responses( 40 42 (status = 200, description = "Value retrieved", body = GetValueResponse), 41 43 (status = 401, description = "Not authenticated"), ··· 43 45 (status = 404, description = "Store or key not found"), 44 46 (status = 500, description = "Internal server error"), 45 47 ), 46 - tag = "kv" 48 + tag = "KV" 47 49 )] 48 50 pub async fn get_value_handler( 49 51 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/list_keys.rs
··· 44 44 ("store_name" = String, Path, description = "Store name"), 45 45 ListKeysQuery 46 46 ), 47 + summary = "List keys", 48 + description = "Returns keys in a store. Use cursor for pagination when list_complete is false.", 47 49 responses( 48 50 (status = 200, description = "List of keys", body = ListKeysResponse), 49 51 (status = 401, description = "Not authenticated"), ··· 51 53 (status = 404, description = "Store not found"), 52 54 (status = 500, description = "Internal server error"), 53 55 ), 54 - tag = "kv" 56 + tag = "KV" 55 57 )] 56 58 pub async fn list_keys_handler( 57 59 State(state): State<AppState>,
+3 -1
apps/runtime/src/handlers/kv/list_stores.rs
··· 27 27 params( 28 28 ListStoresQuery 29 29 ), 30 + summary = "List stores", 31 + description = "Returns all key-value stores for the specified guild.", 30 32 responses( 31 33 (status = 200, description = "List of stores", body = Vec<KvStore>), 32 34 (status = 401, description = "Not authenticated"), 33 35 (status = 403, description = "Not guild admin"), 34 36 (status = 500, description = "Internal server error"), 35 37 ), 36 - tag = "kv" 38 + tag = "KV" 37 39 )] 38 40 pub async fn list_stores_handler( 39 41 State(state): State<AppState>,
+1 -1
apps/runtime/src/handlers/kv/mod.rs
··· 66 66 ExportGuildParams, 67 67 )), 68 68 tags( 69 - (name = "kv", description = "Key-value store management endpoints") 69 + (name = "KV", description = "Key-value store management endpoints") 70 70 ) 71 71 )] 72 72 pub struct KvApi;
+6 -1
apps/runtime/src/handlers/kv/set.rs
··· 24 24 25 25 #[derive(Debug, Deserialize, ToSchema)] 26 26 pub struct SetValueRequest { 27 + /// Raw value to store. 27 28 pub value: String, 29 + /// Unix timestamp in seconds when the key expires, or null for no expiry. 28 30 pub expiration: Option<i64>, 31 + /// Optional JSON metadata attached to the key. 29 32 pub metadata: Option<serde_json::Value>, 30 33 } 31 34 ··· 42 45 ("store_name" = String, Path, description = "Store name"), 43 46 ("key" = String, Path, description = "Key to set"), 44 47 ), 48 + summary = "Set a value", 49 + description = "Creates or replaces a value in the store. Optional expiration controls TTL.", 45 50 request_body = SetValueRequest, 46 51 responses( 47 52 (status = 200, description = "Value set successfully", body = SetValueResponse), ··· 51 56 (status = 404, description = "Store not found"), 52 57 (status = 500, description = "Internal server error"), 53 58 ), 54 - tag = "kv" 59 + tag = "KV" 55 60 )] 56 61 pub async fn set_value_handler( 57 62 State(state): State<AppState>,
+7 -3
apps/runtime/src/handlers/logs.rs
··· 44 44 get, 45 45 path = "", 46 46 params(LogsQuery), 47 + summary = "List logs", 48 + description = "Returns recent log entries visible to the authenticated user.", 47 49 responses( 48 50 (status = 200, description = "Recent log entries", body = Vec<LogEntry>) 49 51 ), 50 - tag = "logs" 52 + tag = "Logs" 51 53 )] 52 54 pub async fn get_logs( 53 55 State(state): State<AppState>, ··· 88 90 get, 89 91 path = "/{guild_id}", 90 92 params( 91 - ("guild_id" = String, Path, description = "Discord guild ID"), 93 + ("guild_id" = String, Path, description = "Guild ID"), 92 94 LogsQuery 93 95 ), 96 + summary = "List guild logs", 97 + description = "Returns recent log entries for a specific guild.", 94 98 responses( 95 99 (status = 200, description = "Recent log entries for guild", body = Vec<LogEntry>) 96 100 ), 97 - tag = "logs" 101 + tag = "Logs" 98 102 )] 99 103 pub async fn get_guild_logs( 100 104 State(state): State<AppState>,
+2 -2
apps/runtime/src/handlers/metrics.rs
··· 27 27 responses( 28 28 (status = 200, description = "Prometheus metrics", content_type = "text/plain") 29 29 ), 30 - tag = "metrics" 30 + tag = "Metrics" 31 31 )] 32 32 pub async fn get_metrics( 33 33 State(state): State<AppState>, ··· 49 49 responses( 50 50 (status = 200, description = "Metrics as JSON", body = metrics::MetricsSnapshot) 51 51 ), 52 - tag = "metrics" 52 + tag = "Metrics" 53 53 )] 54 54 pub async fn get_metrics_json( 55 55 State(state): State<AppState>,
+4 -2
apps/runtime/src/handlers/secrets/delete_secret.rs
··· 23 23 #[utoipa::path( 24 24 delete, 25 25 path = "/{guild_id}/{name}", 26 - tag = "secrets", 26 + tag = "Secrets", 27 + summary = "Delete a secret", 28 + description = "Deletes a secret and refreshes the runtime to remove it.", 27 29 params( 28 - ("guild_id" = String, Path, description = "Discord guild id"), 30 + ("guild_id" = String, Path, description = "Guild ID"), 29 31 ("name" = String, Path, description = "Secret name") 30 32 ), 31 33 responses(
+4 -2
apps/runtime/src/handlers/secrets/list.rs
··· 35 35 #[utoipa::path( 36 36 get, 37 37 path = "/{guild_id}", 38 - tag = "secrets", 38 + tag = "Secrets", 39 + summary = "List secrets", 40 + description = "Returns metadata for all secrets stored for a guild. Values are never returned.", 39 41 params( 40 - ("guild_id" = String, Path, description = "Discord guild id") 42 + ("guild_id" = String, Path, description = "Guild ID") 41 43 ), 42 44 responses( 43 45 (status = 200, description = "List of secret names", body = [SecretMetadataResponse]),
+1 -1
apps/runtime/src/handlers/secrets/mod.rs
··· 25 25 SecretMetadataResponse, 26 26 UpsertSecretRequest, 27 27 )), 28 - tags((name = "secrets", description = "Manage per-guild secrets")) 28 + tags((name = "Secrets", description = "Manage per-guild secrets")) 29 29 )] 30 30 pub struct SecretsApi; 31 31
+4 -2
apps/runtime/src/handlers/secrets/upsert.rs
··· 25 25 #[utoipa::path( 26 26 put, 27 27 path = "/{guild_id}/{name}", 28 - tag = "secrets", 28 + tag = "Secrets", 29 + summary = "Upsert a secret", 30 + description = "Creates or updates a secret value for a guild and refreshes the runtime.", 29 31 params( 30 - ("guild_id" = String, Path, description = "Discord guild id"), 32 + ("guild_id" = String, Path, description = "Guild ID"), 31 33 ("name" = String, Path, description = "Secret name") 32 34 ), 33 35 request_body = UpsertSecretRequest,
+15 -5
apps/runtime/src/handlers/tokens.rs
··· 18 18 #[openapi( 19 19 paths(create_token_handler, list_tokens_handler, delete_token_handler), 20 20 components(schemas(CreateTokenRequest, CreateTokenResponse, TokenResponse, crate::handlers::error::ErrorResponse)), 21 - tags((name = "tokens", description = "User API tokens")) 21 + tags((name = "Tokens", description = "User API tokens")) 22 22 )] 23 23 pub struct TokenApi; 24 24 ··· 41 41 pub token: String, 42 42 } 43 43 44 - /// Token list item. 44 + /// Token metadata returned for list endpoints. 45 45 #[derive(Debug, Serialize, ToSchema)] 46 46 pub struct TokenResponse { 47 + /// Token identifier. 47 48 pub token_id: String, 49 + /// Optional user-facing label for the token. 48 50 pub label: Option<String>, 51 + /// Token creation time in RFC3339 (UTC). 49 52 pub created_at: String, 53 + /// Last usage time in RFC3339 (UTC), if available. 50 54 pub last_used_at: Option<String>, 51 55 } 52 56 ··· 65 69 #[utoipa::path( 66 70 post, 67 71 path = "/", 68 - tag = "tokens", 72 + tag = "Tokens", 73 + summary = "Create a token", 74 + description = "Creates a new API token for the authenticated user. The plaintext token is returned only once.", 69 75 request_body = CreateTokenRequest, 70 76 responses( 71 77 (status = 200, description = "Token created", body = CreateTokenResponse), ··· 91 97 #[utoipa::path( 92 98 get, 93 99 path = "/", 94 - tag = "tokens", 100 + tag = "Tokens", 101 + summary = "List tokens", 102 + description = "Returns metadata for all API tokens owned by the authenticated user.", 95 103 responses( 96 104 (status = 200, description = "Tokens listed", body = [TokenResponse]), 97 105 (status = 401, description = "Unauthorized", body = crate::handlers::error::ErrorResponse) ··· 116 124 #[utoipa::path( 117 125 delete, 118 126 path = "/{token_id}", 119 - tag = "tokens", 127 + tag = "Tokens", 128 + summary = "Delete a token", 129 + description = "Deletes the specified token. Requests using this token stop authenticating immediately.", 120 130 params( 121 131 ("token_id" = String, Path, description = "Token identifier") 122 132 ),