feat: migrate auth and workspace catalog to ServerApi
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
use crate::BackendOrigin;
|
use crate::BackendOrigin;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use server_api::{DeviceLoginPollRequest, DeviceLoginPollStatus, DeviceLoginStartRequest};
|
use server_api::{
|
||||||
|
DeviceLoginPollRequest, DeviceLoginPollStatus, DeviceLoginStartRequest, RepositoryApiError,
|
||||||
|
ServerApiClient,
|
||||||
|
};
|
||||||
pub use server_api::{DeviceLoginPollResponse, DeviceLoginStartResponse};
|
pub use server_api::{DeviceLoginPollResponse, DeviceLoginStartResponse};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -19,21 +21,12 @@ impl BackendAuthTarget {
|
|||||||
.unwrap_or(base_url);
|
.unwrap_or(base_url);
|
||||||
Self { base_url }
|
Self { base_url }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_url(&self, path: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"{}{}",
|
|
||||||
self.base_url.trim_end_matches('/'),
|
|
||||||
path.strip_prefix('/')
|
|
||||||
.map(|path| format!("/{path}"))
|
|
||||||
.unwrap_or_else(|| path.to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum BackendAuthClientError {
|
pub enum BackendAuthClientError {
|
||||||
Http(reqwest::Error),
|
InvalidTarget(String),
|
||||||
|
ServerApi(server_api::client_support::ClientError<RepositoryApiError>),
|
||||||
BackendStatus { status: u16, body: String },
|
BackendStatus { status: u16, body: String },
|
||||||
MissingAccessToken,
|
MissingAccessToken,
|
||||||
}
|
}
|
||||||
@@ -41,7 +34,8 @@ pub enum BackendAuthClientError {
|
|||||||
impl fmt::Display for BackendAuthClientError {
|
impl fmt::Display for BackendAuthClientError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Http(error) => write!(f, "Backend auth request failed: {error}"),
|
Self::InvalidTarget(message) => f.write_str(message),
|
||||||
|
Self::ServerApi(error) => write!(f, "{error}"),
|
||||||
Self::BackendStatus { status, body } => {
|
Self::BackendStatus { status, body } => {
|
||||||
write!(f, "Backend auth returned HTTP {status}: {body}")
|
write!(f, "Backend auth returned HTTP {status}: {body}")
|
||||||
}
|
}
|
||||||
@@ -54,40 +48,38 @@ impl fmt::Display for BackendAuthClientError {
|
|||||||
|
|
||||||
impl std::error::Error for BackendAuthClientError {}
|
impl std::error::Error for BackendAuthClientError {}
|
||||||
|
|
||||||
impl From<reqwest::Error> for BackendAuthClientError {
|
const AUTH_RESPONSE_LIMIT: usize = 2 * 1024 * 1024;
|
||||||
fn from(value: reqwest::Error) -> Self {
|
|
||||||
Self::Http(value)
|
fn auth_client(target: &BackendAuthTarget) -> Result<ServerApiClient, BackendAuthClientError> {
|
||||||
}
|
ServerApiClient::builder(&target.base_url)
|
||||||
|
.map_err(|error| BackendAuthClientError::InvalidTarget(error.to_string()))?
|
||||||
|
.response_body_limit(AUTH_RESPONSE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendAuthClientError::InvalidTarget(error.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_device_login(
|
pub async fn start_device_login(
|
||||||
target: &BackendAuthTarget,
|
target: &BackendAuthTarget,
|
||||||
client_name: Option<&str>,
|
client_name: Option<&str>,
|
||||||
) -> Result<DeviceLoginStartResponse, BackendAuthClientError> {
|
) -> Result<DeviceLoginStartResponse, BackendAuthClientError> {
|
||||||
let client = reqwest::Client::new();
|
auth_client(target)?
|
||||||
let response = client
|
.auth_device_login_start(DeviceLoginStartRequest {
|
||||||
.post(target.api_url("/api/auth/device-login/start"))
|
|
||||||
.json(&DeviceLoginStartRequest {
|
|
||||||
client_name: client_name.map(ToOwned::to_owned),
|
client_name: client_name.map(ToOwned::to_owned),
|
||||||
})
|
})
|
||||||
.send()
|
.await
|
||||||
.await?;
|
.map_err(BackendAuthClientError::ServerApi)
|
||||||
parse_json_response(response).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn poll_device_login(
|
pub async fn poll_device_login(
|
||||||
target: &BackendAuthTarget,
|
target: &BackendAuthTarget,
|
||||||
device_code: &str,
|
device_code: &str,
|
||||||
) -> Result<DeviceLoginPollResponse, BackendAuthClientError> {
|
) -> Result<DeviceLoginPollResponse, BackendAuthClientError> {
|
||||||
let client = reqwest::Client::new();
|
auth_client(target)?
|
||||||
let response = client
|
.auth_device_login_poll(DeviceLoginPollRequest {
|
||||||
.post(target.api_url("/api/auth/device-login/poll"))
|
|
||||||
.json(&DeviceLoginPollRequest {
|
|
||||||
device_code: device_code.to_string(),
|
device_code: device_code.to_string(),
|
||||||
})
|
})
|
||||||
.send()
|
.await
|
||||||
.await?;
|
.map_err(BackendAuthClientError::ServerApi)
|
||||||
parse_json_response(response).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn device_login_poll_result(
|
fn device_login_poll_result(
|
||||||
@@ -136,24 +128,13 @@ pub async fn wait_for_device_login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_json_response<T: for<'de> Deserialize<'de>>(
|
|
||||||
response: reqwest::Response,
|
|
||||||
) -> Result<T, BackendAuthClientError> {
|
|
||||||
let status = response.status();
|
|
||||||
if !status.is_success() {
|
|
||||||
let body = response.text().await.unwrap_or_default();
|
|
||||||
return Err(BackendAuthClientError::BackendStatus {
|
|
||||||
status: status.as_u16(),
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(response.json::<T>().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use server_api::DeviceAccessTokenType;
|
use server_api::DeviceAccessTokenType;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
fn poll_response(status: DeviceLoginPollStatus) -> DeviceLoginPollResponse {
|
fn poll_response(status: DeviceLoginPollStatus) -> DeviceLoginPollResponse {
|
||||||
DeviceLoginPollResponse {
|
DeviceLoginPollResponse {
|
||||||
@@ -163,6 +144,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn device_login_start_uses_generated_server_api_client() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let base_url = format!("http://{}", listener.local_addr().unwrap());
|
||||||
|
let handle = thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut request = vec![0; 4096];
|
||||||
|
let read = stream.read(&mut request).unwrap();
|
||||||
|
let request = String::from_utf8_lossy(&request[..read]);
|
||||||
|
assert!(request.starts_with("POST /api/auth/device-login/start HTTP/1.1"));
|
||||||
|
assert!(request.contains(r#"{"client_name":"yoi-cli"}"#));
|
||||||
|
let body = r#"{"device_code":"device-secret","user_code":"ABCD-EFGH","verification_uri":"https://yoi.example/login/device","verification_uri_complete":"https://yoi.example/login/device?user_code=ABCD-EFGH","expires_in":600,"interval":5}"#;
|
||||||
|
write!(
|
||||||
|
stream,
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
body.len(),
|
||||||
|
body
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = start_device_login(&BackendAuthTarget::new(base_url), Some("yoi-cli"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.user_code, "ABCD-EFGH");
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn device_login_start_response_enforces_shared_expiry_bounds() {
|
fn device_login_start_response_enforces_shared_expiry_bounds() {
|
||||||
let valid = serde_json::json!({
|
let valid = serde_json::json!({
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::{BackendApiClient, BackendApiClientError};
|
use crate::{BackendApiClient, BackendApiClientError};
|
||||||
use reqwest::Method;
|
|
||||||
use server_api::{
|
use server_api::{
|
||||||
InitialRepositoryIntent, RepositorySummary, WorkspaceCatalogListResponse,
|
InitialRepositoryIntent, RepositorySummary, WorkspaceCreateRequest, WorkspaceCreateResponse,
|
||||||
WorkspaceCreateRequest, WorkspaceCreateResponse, WorkspaceSummary,
|
WorkspaceListQuery, WorkspaceSummary,
|
||||||
};
|
};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ pub enum BackendWorkspaceClientError {
|
|||||||
InvalidTarget(String),
|
InvalidTarget(String),
|
||||||
Api(BackendApiClientError),
|
Api(BackendApiClientError),
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
RepositoryApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
ServerApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for BackendWorkspaceClientError {
|
impl fmt::Display for BackendWorkspaceClientError {
|
||||||
@@ -40,7 +39,7 @@ impl fmt::Display for BackendWorkspaceClientError {
|
|||||||
Self::InvalidTarget(message) => f.write_str(message),
|
Self::InvalidTarget(message) => f.write_str(message),
|
||||||
Self::Api(error) => write!(f, "{error}"),
|
Self::Api(error) => write!(f, "{error}"),
|
||||||
Self::Http(error) => write!(f, "{error}"),
|
Self::Http(error) => write!(f, "{error}"),
|
||||||
Self::RepositoryApi(error) => write!(f, "{error}"),
|
Self::ServerApi(error) => write!(f, "{error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,11 +64,11 @@ impl From<server_api::client_support::ClientError<server_api::RepositoryApiError
|
|||||||
fn from(
|
fn from(
|
||||||
error: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
error: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::RepositoryApi(error)
|
Self::ServerApi(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repository_client_error(
|
fn server_client_error(
|
||||||
backend: &BackendApiClient,
|
backend: &BackendApiClient,
|
||||||
error: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
error: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||||
) -> BackendWorkspaceClientError {
|
) -> BackendWorkspaceClientError {
|
||||||
@@ -88,16 +87,16 @@ fn repository_client_error(
|
|||||||
origin: backend.origin().clone(),
|
origin: backend.origin().clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => BackendWorkspaceClientError::RepositoryApi(error),
|
_ => BackendWorkspaceClientError::ServerApi(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct RepositoryBearerAuthorizer {
|
struct ServerBearerAuthorizer {
|
||||||
authorization: String,
|
authorization: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl server_api::client_support::RequestAuthorizer for RepositoryBearerAuthorizer {
|
impl server_api::client_support::RequestAuthorizer for ServerBearerAuthorizer {
|
||||||
fn authorize(
|
fn authorize(
|
||||||
&self,
|
&self,
|
||||||
_request: server_api::client_support::AuthorizerRequest<'_>,
|
_request: server_api::client_support::AuthorizerRequest<'_>,
|
||||||
@@ -113,20 +112,31 @@ impl server_api::client_support::RequestAuthorizer for RepositoryBearerAuthorize
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const REPOSITORY_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
const SERVER_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
fn server_api_client(
|
||||||
|
backend: &BackendApiClient,
|
||||||
|
) -> Result<server_api::ServerApiClient<ServerBearerAuthorizer>, BackendWorkspaceClientError> {
|
||||||
|
server_api::ServerApiClient::builder(backend.origin().as_str())
|
||||||
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?
|
||||||
|
.client(backend.asynchronous_client())
|
||||||
|
.authorizer(ServerBearerAuthorizer {
|
||||||
|
authorization: backend.authorization_header_value(),
|
||||||
|
})
|
||||||
|
.response_body_limit(SERVER_RESPONSE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_backend_workspaces_blocking(
|
pub fn list_backend_workspaces_blocking(
|
||||||
target: &BackendWorkspaceCatalogTarget,
|
target: &BackendWorkspaceCatalogTarget,
|
||||||
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
||||||
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
let response = client
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
.blocking_request(
|
.enable_all()
|
||||||
Method::GET,
|
.build()
|
||||||
&format!("/api/workspaces?limit={DEFAULT_WORKSPACE_LIMIT}"),
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
||||||
)?
|
runtime.block_on(list_backend_workspaces_with_client(&client))
|
||||||
.send()?;
|
|
||||||
client.check_status(response.status())?;
|
|
||||||
Ok(response.json::<WorkspaceCatalogListResponse>()?.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_backend_workspace_repositories_blocking(
|
pub fn list_backend_workspace_repositories_blocking(
|
||||||
@@ -158,19 +168,10 @@ async fn list_backend_workspace_repositories_with_client(
|
|||||||
"Workspace id returned by Backend is invalid".to_string(),
|
"Workspace id returned by Backend is invalid".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let client = server_api::ServerApiClient::builder(backend.origin().as_str())
|
let response = server_api_client(backend)?
|
||||||
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?
|
|
||||||
.client(backend.asynchronous_client())
|
|
||||||
.authorizer(RepositoryBearerAuthorizer {
|
|
||||||
authorization: backend.authorization_header_value(),
|
|
||||||
})
|
|
||||||
.response_body_limit(REPOSITORY_RESPONSE_LIMIT)
|
|
||||||
.build()
|
|
||||||
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
|
||||||
let response = client
|
|
||||||
.repository_list(workspace_id.to_owned())
|
.repository_list(workspace_id.to_owned())
|
||||||
.await
|
.await
|
||||||
.map_err(|error| repository_client_error(backend, error))?;
|
.map_err(|error| server_client_error(backend, error))?;
|
||||||
if response.workspace_id != workspace_id {
|
if response.workspace_id != workspace_id {
|
||||||
return Err(BackendWorkspaceClientError::InvalidTarget(
|
return Err(BackendWorkspaceClientError::InvalidTarget(
|
||||||
"Repository catalog response does not match the requested Workspace".to_string(),
|
"Repository catalog response does not match the requested Workspace".to_string(),
|
||||||
@@ -189,15 +190,13 @@ pub async fn list_backend_workspaces(
|
|||||||
async fn list_backend_workspaces_with_client(
|
async fn list_backend_workspaces_with_client(
|
||||||
client: &BackendApiClient,
|
client: &BackendApiClient,
|
||||||
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
||||||
let response = client
|
let response = server_api_client(client)?
|
||||||
.request(
|
.workspace_catalog_list(WorkspaceListQuery {
|
||||||
Method::GET,
|
limit: Some(DEFAULT_WORKSPACE_LIMIT as u32),
|
||||||
&format!("/api/workspaces?limit={DEFAULT_WORKSPACE_LIMIT}"),
|
})
|
||||||
)?
|
.await
|
||||||
.send()
|
.map_err(|error| server_client_error(client, error))?;
|
||||||
.await?;
|
Ok(response.0)
|
||||||
client.check_status(response.status())?;
|
|
||||||
Ok(response.json::<WorkspaceCatalogListResponse>().await?.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_backend_workspace(
|
pub async fn create_backend_workspace(
|
||||||
@@ -205,13 +204,10 @@ pub async fn create_backend_workspace(
|
|||||||
request: &CreateBackendWorkspaceRequest,
|
request: &CreateBackendWorkspaceRequest,
|
||||||
) -> Result<CreateBackendWorkspaceResponse, BackendWorkspaceClientError> {
|
) -> Result<CreateBackendWorkspaceResponse, BackendWorkspaceClientError> {
|
||||||
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
let response = client
|
server_api_client(&client)?
|
||||||
.request(Method::POST, "/api/workspaces")?
|
.workspace_catalog_create(request.clone())
|
||||||
.json(request)
|
.await
|
||||||
.send()
|
.map_err(|error| server_client_error(&client, error))
|
||||||
.await?;
|
|
||||||
client.check_status(response.status())?;
|
|
||||||
Ok(response.json::<CreateBackendWorkspaceResponse>().await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -292,12 +288,12 @@ mod tests {
|
|||||||
write!(
|
write!(
|
||||||
stream,
|
stream,
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
REPOSITORY_RESPONSE_LIMIT + 1,
|
SERVER_RESPONSE_LIMIT + 1,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
stream.flush().unwrap();
|
stream.flush().unwrap();
|
||||||
let chunk = [b' '; 8192];
|
let chunk = [b' '; 8192];
|
||||||
let mut remaining = REPOSITORY_RESPONSE_LIMIT + 1;
|
let mut remaining = SERVER_RESPONSE_LIMIT + 1;
|
||||||
while remaining > 0 {
|
while remaining > 0 {
|
||||||
let length = remaining.min(chunk.len());
|
let length = remaining.min(chunk.len());
|
||||||
if stream.write_all(&chunk[..length]).is_err() {
|
if stream.write_all(&chunk[..length]).is_err() {
|
||||||
@@ -315,10 +311,10 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(
|
||||||
&error,
|
&error,
|
||||||
BackendWorkspaceClientError::RepositoryApi(
|
BackendWorkspaceClientError::ServerApi(
|
||||||
server_api::client_support::ClientError::Failure(
|
server_api::client_support::ClientError::Failure(
|
||||||
server_api::client_support::ClientFailure::ResponseTooLarge {
|
server_api::client_support::ClientFailure::ResponseTooLarge {
|
||||||
limit: REPOSITORY_RESPONSE_LIMIT
|
limit: SERVER_RESPONSE_LIMIT
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -329,11 +325,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repository_client_preserves_authentication_error_taxonomy() {
|
fn server_client_preserves_authentication_error_taxonomy() {
|
||||||
let client =
|
let client =
|
||||||
BackendApiClient::from_access_token_for_test("https://backend.example.test", "token")
|
BackendApiClient::from_access_token_for_test("https://backend.example.test", "token")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let unauthorized = repository_client_error(
|
let unauthorized = server_client_error(
|
||||||
&client,
|
&client,
|
||||||
server_api::client_support::ClientError::public(
|
server_api::client_support::ClientError::public(
|
||||||
reqwest::StatusCode::UNAUTHORIZED,
|
reqwest::StatusCode::UNAUTHORIZED,
|
||||||
@@ -349,7 +345,7 @@ mod tests {
|
|||||||
unauthorized,
|
unauthorized,
|
||||||
BackendWorkspaceClientError::Api(BackendApiClientError::Unauthorized { .. })
|
BackendWorkspaceClientError::Api(BackendApiClientError::Unauthorized { .. })
|
||||||
));
|
));
|
||||||
let forbidden = repository_client_error(
|
let forbidden = server_client_error(
|
||||||
&client,
|
&client,
|
||||||
server_api::client_support::ClientError::public(
|
server_api::client_support::ClientError::public(
|
||||||
reqwest::StatusCode::FORBIDDEN,
|
reqwest::StatusCode::FORBIDDEN,
|
||||||
|
|||||||
+399
-34
@@ -22,6 +22,7 @@ pub type ServerApiClientError = client_support::ClientError<ServerApiError>;
|
|||||||
|
|
||||||
/// Error body shared by the existing repository routes and their generated adapters.
|
/// Error body shared by the existing repository routes and their generated adapters.
|
||||||
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
|
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RepositoryApiError {
|
pub struct RepositoryApiError {
|
||||||
pub error: String,
|
pub error: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
@@ -76,6 +77,28 @@ macro_rules! impl_openapi_schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_openapi_schema!(
|
impl_openapi_schema!(
|
||||||
|
AuthBootstrapUserRequest,
|
||||||
|
AuthPublicConfig,
|
||||||
|
AuthUserResponse,
|
||||||
|
DeviceLoginApproveRequest,
|
||||||
|
DeviceLoginApproveResponse,
|
||||||
|
DeviceLoginPollRequest,
|
||||||
|
DeviceLoginPollResponse,
|
||||||
|
DeviceLoginStartRequest,
|
||||||
|
DeviceLoginStartResponse,
|
||||||
|
HealthResponse,
|
||||||
|
PasskeyLoginOptionsRequest,
|
||||||
|
PasskeyLoginOptionsResponse,
|
||||||
|
PasskeyRegistrationOptionsRequest,
|
||||||
|
PasskeyRegistrationOptionsResponse,
|
||||||
|
WhoamiResponse,
|
||||||
|
WorkspaceCreateRequest,
|
||||||
|
WorkspaceCreateResponse,
|
||||||
|
WorkspaceDeletionOperationResponse,
|
||||||
|
WorkspaceDeletionPreflightResponse,
|
||||||
|
WorkspaceDeletionRequest,
|
||||||
|
WorkspaceListQuery,
|
||||||
|
WorkspaceCatalogListResponse,
|
||||||
CreateWorkspaceRepositoryRequest,
|
CreateWorkspaceRepositoryRequest,
|
||||||
CreateWorkspaceRepositoryResponse,
|
CreateWorkspaceRepositoryResponse,
|
||||||
RepositoryApiError,
|
RepositoryApiError,
|
||||||
@@ -92,6 +115,162 @@ pub struct WorkspaceWorkerSessionResponse {
|
|||||||
|
|
||||||
#[api(reqwest, axum, openapi)]
|
#[api(reqwest, axum, openapi)]
|
||||||
pub trait ServerApi {
|
pub trait ServerApi {
|
||||||
|
#[get("/health", status = 200, error_status = 400)]
|
||||||
|
async fn health(&self) -> Result<HealthResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get("/api/auth/config", status = 200, error_status = 400)]
|
||||||
|
async fn auth_config(&self) -> Result<AuthPublicConfig, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/bootstrap-user",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [500]
|
||||||
|
)]
|
||||||
|
async fn auth_bootstrap_user(
|
||||||
|
&self,
|
||||||
|
#[body] request: AuthBootstrapUserRequest,
|
||||||
|
) -> Result<AuthUserResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/passkeys/registration/options",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [500, 502]
|
||||||
|
)]
|
||||||
|
async fn auth_passkey_registration_options(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[body] request: PasskeyRegistrationOptionsRequest,
|
||||||
|
) -> Result<PasskeyRegistrationOptionsResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/passkeys/login/options",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [500, 502]
|
||||||
|
)]
|
||||||
|
async fn auth_passkey_login_options(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[body] request: PasskeyLoginOptionsRequest,
|
||||||
|
) -> Result<PasskeyLoginOptionsResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/device-login/start",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [500, 502]
|
||||||
|
)]
|
||||||
|
async fn auth_device_login_start(
|
||||||
|
&self,
|
||||||
|
#[body] request: DeviceLoginStartRequest,
|
||||||
|
) -> Result<DeviceLoginStartResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/device-login/approve",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [401, 403, 500, 502],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn auth_device_login_approve(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[body] request: DeviceLoginApproveRequest,
|
||||||
|
) -> Result<DeviceLoginApproveResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/auth/device-login/poll",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [500, 502]
|
||||||
|
)]
|
||||||
|
async fn auth_device_login_poll(
|
||||||
|
&self,
|
||||||
|
#[body] request: DeviceLoginPollRequest,
|
||||||
|
) -> Result<DeviceLoginPollResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get("/api/auth/whoami", status = 200, error_status = 400)]
|
||||||
|
async fn auth_whoami(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
) -> Result<WhoamiResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get(
|
||||||
|
"/api/workspaces",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [401, 403, 500],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn workspace_catalog_list(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[query] query: WorkspaceListQuery,
|
||||||
|
) -> Result<WorkspaceCatalogListResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/workspaces",
|
||||||
|
status = 201,
|
||||||
|
alternate_status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [401, 403, 409, 500],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn workspace_catalog_create(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[body] request: WorkspaceCreateRequest,
|
||||||
|
) -> Result<WorkspaceCreateResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get(
|
||||||
|
"/api/workspaces/{workspace_id}/deletion",
|
||||||
|
status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [401, 403, 404, 500],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn workspace_deletion_preflight(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[path] workspace_id: String,
|
||||||
|
) -> Result<WorkspaceDeletionPreflightResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/workspaces/{workspace_id}/deletion",
|
||||||
|
status = 202,
|
||||||
|
alternate_status = 200,
|
||||||
|
error_status = 400,
|
||||||
|
additional_error_statuses = [401, 403, 404, 409, 500],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn workspace_deletion_start(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[path] workspace_id: String,
|
||||||
|
#[body] request: WorkspaceDeletionRequest,
|
||||||
|
) -> Result<WorkspaceDeletionOperationResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get(
|
||||||
|
"/api/workspace-deletions/{operation_id}",
|
||||||
|
status = 200,
|
||||||
|
error_status = 404,
|
||||||
|
additional_error_statuses = [400, 401, 403, 500],
|
||||||
|
bearer_auth = true,
|
||||||
|
browser_auth = true
|
||||||
|
)]
|
||||||
|
async fn workspace_deletion_get(
|
||||||
|
&self,
|
||||||
|
#[extension] context: ServerRequestContext,
|
||||||
|
#[path] operation_id: String,
|
||||||
|
) -> Result<WorkspaceDeletionOperationResponse, RepositoryApiError>;
|
||||||
|
|
||||||
#[get(
|
#[get(
|
||||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/session",
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/session",
|
||||||
status = 200,
|
status = 200,
|
||||||
@@ -204,8 +383,30 @@ pub fn canonical_openapi_document()
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server-local request context populated by authentication middleware.
|
||||||
|
///
|
||||||
|
/// Generated clients and OpenAPI omit extension parameters; operation implementations use this
|
||||||
|
/// value instead of re-parsing transport headers inside domain handlers.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServerRequestContext {
|
||||||
|
pub actor: Option<RequestActor>,
|
||||||
|
pub origin: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct HealthResponse {
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceListQuery {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Public browser-authentication configuration.
|
/// Public browser-authentication configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AuthPublicConfig {
|
pub struct AuthPublicConfig {
|
||||||
@@ -216,7 +417,7 @@ pub struct AuthPublicConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authentication method that established the current request actor.
|
/// Authentication method that established the current request actor.
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ActorAuthMethod {
|
pub enum ActorAuthMethod {
|
||||||
@@ -225,7 +426,7 @@ pub enum ActorAuthMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Public user identity returned by authentication operations.
|
/// Public user identity returned by authentication operations.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AuthenticatedUser {
|
pub struct AuthenticatedUser {
|
||||||
@@ -236,7 +437,7 @@ pub struct AuthenticatedUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticated actor returned by `GET /api/auth/whoami`.
|
/// Authenticated actor returned by `GET /api/auth/whoami`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RequestActor {
|
pub struct RequestActor {
|
||||||
@@ -258,14 +459,14 @@ impl RequestActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WhoamiResponse {
|
pub struct WhoamiResponse {
|
||||||
pub actor: Option<RequestActor>,
|
pub actor: Option<RequestActor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AuthBootstrapUserRequest {
|
pub struct AuthBootstrapUserRequest {
|
||||||
@@ -275,14 +476,14 @@ pub struct AuthBootstrapUserRequest {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AuthUserResponse {
|
pub struct AuthUserResponse {
|
||||||
pub user: AuthenticatedUser,
|
pub user: AuthenticatedUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PasskeyRegistrationOptionsRequest {
|
pub struct PasskeyRegistrationOptionsRequest {
|
||||||
@@ -295,12 +496,13 @@ pub struct PasskeyRegistrationOptionsRequest {
|
|||||||
pub browser_origin: Option<String>,
|
pub browser_origin: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PasskeyRegistrationOptionsResponse {
|
pub struct PasskeyRegistrationOptionsResponse {
|
||||||
pub challenge_id: String,
|
pub challenge_id: String,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
|
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
|
||||||
|
#[schemars(with = "serde_json::Value")]
|
||||||
pub public_key: CreationChallengeResponse,
|
pub public_key: CreationChallengeResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +515,7 @@ pub struct PasskeyRegistrationCompleteRequest {
|
|||||||
pub credential: RegisterPublicKeyCredential,
|
pub credential: RegisterPublicKeyCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PasskeyLoginOptionsRequest {
|
pub struct PasskeyLoginOptionsRequest {
|
||||||
@@ -325,12 +527,13 @@ pub struct PasskeyLoginOptionsRequest {
|
|||||||
pub browser_origin: Option<String>,
|
pub browser_origin: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PasskeyLoginOptionsResponse {
|
pub struct PasskeyLoginOptionsResponse {
|
||||||
pub challenge_id: String,
|
pub challenge_id: String,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
|
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
|
||||||
|
#[schemars(with = "serde_json::Value")]
|
||||||
pub public_key: RequestChallengeResponse,
|
pub public_key: RequestChallengeResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +546,7 @@ pub struct PasskeyLoginCompleteRequest {
|
|||||||
pub credential: PublicKeyCredential,
|
pub credential: PublicKeyCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginStartRequest {
|
pub struct DeviceLoginStartRequest {
|
||||||
@@ -355,16 +558,19 @@ pub struct DeviceLoginStartRequest {
|
|||||||
const DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS: u64 = 24 * 60 * 60;
|
const DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS: u64 = 24 * 60 * 60;
|
||||||
const DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS: u64 = 60;
|
const DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS: u64 = 60;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginStartResponse {
|
pub struct DeviceLoginStartResponse {
|
||||||
pub device_code: String,
|
pub device_code: String,
|
||||||
pub user_code: String,
|
pub user_code: String,
|
||||||
pub verification_uri: String,
|
pub verification_uri: String,
|
||||||
pub verification_uri_complete: String,
|
pub verification_uri_complete: String,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 1, max = 86400))]
|
||||||
pub expires_in: u64,
|
pub expires_in: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 1, max = 60))]
|
||||||
pub interval: u64,
|
pub interval: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,21 +626,21 @@ impl std::fmt::Debug for DeviceLoginStartResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginApproveRequest {
|
pub struct DeviceLoginApproveRequest {
|
||||||
pub user_code: String,
|
pub user_code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum DeviceLoginApprovalStatus {
|
pub enum DeviceLoginApprovalStatus {
|
||||||
Approved,
|
Approved,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginApproveResponse {
|
pub struct DeviceLoginApproveResponse {
|
||||||
@@ -442,20 +648,20 @@ pub struct DeviceLoginApproveResponse {
|
|||||||
pub user: AuthenticatedUser,
|
pub user: AuthenticatedUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginPollRequest {
|
pub struct DeviceLoginPollRequest {
|
||||||
pub device_code: String,
|
pub device_code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub enum DeviceAccessTokenType {
|
pub enum DeviceAccessTokenType {
|
||||||
Bearer,
|
Bearer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum DeviceLoginPollStatus {
|
pub enum DeviceLoginPollStatus {
|
||||||
@@ -466,7 +672,7 @@ pub enum DeviceLoginPollStatus {
|
|||||||
Consumed,
|
Consumed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DeviceLoginPollResponse {
|
pub struct DeviceLoginPollResponse {
|
||||||
@@ -680,7 +886,7 @@ impl RepositoryObservedStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Public Workspace catalog item returned by `GET /api/workspaces`.
|
/// Public Workspace catalog item returned by `GET /api/workspaces`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceSummary {
|
pub struct WorkspaceSummary {
|
||||||
@@ -696,12 +902,12 @@ pub struct WorkspaceSummary {
|
|||||||
///
|
///
|
||||||
/// The transparent newtype keeps the established top-level JSON array while making the
|
/// The transparent newtype keeps the established top-level JSON array while making the
|
||||||
/// complete list response a named cross-crate and generated-TypeScript authority.
|
/// complete list response a named cross-crate and generated-TypeScript authority.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct WorkspaceCatalogListResponse(pub Vec<WorkspaceSummary>);
|
pub struct WorkspaceCatalogListResponse(pub Vec<WorkspaceSummary>);
|
||||||
|
|
||||||
/// Public Repository record embedded in Workspace creation responses.
|
/// Public Repository record embedded in Workspace creation responses.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceRepositoryRecord {
|
pub struct WorkspaceRepositoryRecord {
|
||||||
@@ -712,6 +918,7 @@ pub struct WorkspaceRepositoryRecord {
|
|||||||
pub source: RepositorySource,
|
pub source: RepositorySource,
|
||||||
pub default_ref: Option<String>,
|
pub default_ref: Option<String>,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub source_revision: u64,
|
pub source_revision: u64,
|
||||||
pub source_fingerprint: String,
|
pub source_fingerprint: String,
|
||||||
pub observed_status: RepositoryObservedStatus,
|
pub observed_status: RepositoryObservedStatus,
|
||||||
@@ -721,7 +928,7 @@ pub struct WorkspaceRepositoryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Initial Repository registration intent for Workspace creation.
|
/// Initial Repository registration intent for Workspace creation.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct InitialRepositoryIntent {
|
pub struct InitialRepositoryIntent {
|
||||||
pub repository_key: String,
|
pub repository_key: String,
|
||||||
@@ -731,7 +938,7 @@ pub struct InitialRepositoryIntent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Request for atomically creating a Workspace and its initial Repository.
|
/// Request for atomically creating a Workspace and its initial Repository.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceCreateRequest {
|
pub struct WorkspaceCreateRequest {
|
||||||
pub operation_key: String,
|
pub operation_key: String,
|
||||||
@@ -740,18 +947,25 @@ pub struct WorkspaceCreateRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Response returned after atomically creating a Workspace and its first Repository.
|
/// Response returned after atomically creating a Workspace and its first Repository.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceCreateResponse {
|
pub struct WorkspaceCreateResponse {
|
||||||
pub workspace: WorkspaceSummary,
|
pub workspace: WorkspaceSummary,
|
||||||
pub repository: WorkspaceRepositoryRecord,
|
pub repository: WorkspaceRepositoryRecord,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub config_revision: u64,
|
pub config_revision: u64,
|
||||||
pub request_fingerprint: String,
|
pub request_fingerprint: String,
|
||||||
pub replayed: bool,
|
pub replayed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl api_macros::HttpSuccess for WorkspaceCreateResponse {
|
||||||
|
fn status_code(&self) -> u16 {
|
||||||
|
if self.replayed { 200 } else { 201 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Browser authentication configuration exposed by the scoped Workspace summary.
|
/// Browser authentication configuration exposed by the scoped Workspace summary.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
@@ -1016,7 +1230,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lifecycle state for one durable Workspace deletion operation.
|
/// Lifecycle state for one durable Workspace deletion operation.
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum WorkspaceDeletionState {
|
pub enum WorkspaceDeletionState {
|
||||||
@@ -1028,7 +1242,7 @@ pub enum WorkspaceDeletionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stable category explaining why Workspace deletion cannot currently advance.
|
/// Stable category explaining why Workspace deletion cannot currently advance.
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum WorkspaceDeletionBlockerKind {
|
pub enum WorkspaceDeletionBlockerKind {
|
||||||
@@ -1042,8 +1256,9 @@ pub enum WorkspaceDeletionBlockerKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One bounded, user-actionable blocker returned by preflight or execution.
|
/// One bounded, user-actionable blocker returned by preflight or execution.
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceDeletionBlocker {
|
pub struct WorkspaceDeletionBlocker {
|
||||||
pub kind: WorkspaceDeletionBlockerKind,
|
pub kind: WorkspaceDeletionBlockerKind,
|
||||||
pub resource_kind: Option<String>,
|
pub resource_kind: Option<String>,
|
||||||
@@ -1052,27 +1267,34 @@ pub struct WorkspaceDeletionBlocker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Workspace-owned resources summarized before destructive confirmation.
|
/// Workspace-owned resources summarized before destructive confirmation.
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceDeletionResourceCounts {
|
pub struct WorkspaceDeletionResourceCounts {
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub workers: u64,
|
pub workers: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub workdirs: u64,
|
pub workdirs: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub repositories: u64,
|
pub repositories: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub runtime_bindings: u64,
|
pub runtime_bindings: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub secrets: u64,
|
pub secrets: u64,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub artifacts: u64,
|
pub artifacts: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owner-only impact preview for deleting one Workspace.
|
/// Owner-only impact preview for deleting one Workspace.
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceDeletionPreflightResponse {
|
pub struct WorkspaceDeletionPreflightResponse {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
@@ -1084,8 +1306,9 @@ pub struct WorkspaceDeletionPreflightResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotent request to start or resume Workspace deletion.
|
/// Idempotent request to start or resume Workspace deletion.
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceDeletionRequest {
|
pub struct WorkspaceDeletionRequest {
|
||||||
pub operation_id: String,
|
pub operation_id: String,
|
||||||
pub expected_revision: String,
|
pub expected_revision: String,
|
||||||
@@ -1093,8 +1316,9 @@ pub struct WorkspaceDeletionRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Durable deletion operation projection used by request responses and polling.
|
/// Durable deletion operation projection used by request responses and polling.
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkspaceDeletionOperationResponse {
|
pub struct WorkspaceDeletionOperationResponse {
|
||||||
pub operation_id: String,
|
pub operation_id: String,
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@@ -1109,6 +1333,16 @@ pub struct WorkspaceDeletionOperationResponse {
|
|||||||
pub completed_at: Option<String>,
|
pub completed_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl api_macros::HttpSuccess for WorkspaceDeletionOperationResponse {
|
||||||
|
fn status_code(&self) -> u16 {
|
||||||
|
if self.state == WorkspaceDeletionState::Succeeded {
|
||||||
|
200
|
||||||
|
} else {
|
||||||
|
202
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct WorkspaceDeletionBlockerWire {
|
struct WorkspaceDeletionBlockerWire {
|
||||||
@@ -3881,7 +4115,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn worker_session_contract_and_flattened_availability_are_stable() {
|
fn worker_session_contract_and_flattened_availability_are_stable() {
|
||||||
let operations = ServerApiMetadata::OPERATIONS;
|
let operations = ServerApiMetadata::OPERATIONS;
|
||||||
assert_eq!(operations.len(), 6);
|
|
||||||
let operation = operations
|
let operation = operations
|
||||||
.iter()
|
.iter()
|
||||||
.find(|operation| operation.operation_id == "worker_session")
|
.find(|operation| operation.operation_id == "worker_session")
|
||||||
@@ -3904,6 +4137,69 @@ mod tests {
|
|||||||
assert_eq!(value["subject"]["kind"], "runtime_worker");
|
assert_eq!(value["subject"]["kind"], "runtime_worker");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_and_workspace_catalog_operations_are_in_server_api_metadata() {
|
||||||
|
let operations = ServerApiMetadata::OPERATIONS;
|
||||||
|
for (operation_id, method, path) in [
|
||||||
|
("health", HttpMethod::Get, "/health"),
|
||||||
|
("auth_config", HttpMethod::Get, "/api/auth/config"),
|
||||||
|
(
|
||||||
|
"auth_passkey_registration_options",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/auth/passkeys/registration/options",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auth_passkey_login_options",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/auth/passkeys/login/options",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auth_device_login_start",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/auth/device-login/start",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auth_device_login_approve",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/auth/device-login/approve",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auth_device_login_poll",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/auth/device-login/poll",
|
||||||
|
),
|
||||||
|
("auth_whoami", HttpMethod::Get, "/api/auth/whoami"),
|
||||||
|
("workspace_catalog_list", HttpMethod::Get, "/api/workspaces"),
|
||||||
|
(
|
||||||
|
"workspace_catalog_create",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/workspaces",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace_deletion_preflight",
|
||||||
|
HttpMethod::Get,
|
||||||
|
"/api/workspaces/{workspace_id}/deletion",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace_deletion_start",
|
||||||
|
HttpMethod::Post,
|
||||||
|
"/api/workspaces/{workspace_id}/deletion",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace_deletion_get",
|
||||||
|
HttpMethod::Get,
|
||||||
|
"/api/workspace-deletions/{operation_id}",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let operation = operations
|
||||||
|
.iter()
|
||||||
|
.find(|operation| operation.operation_id == operation_id)
|
||||||
|
.unwrap_or_else(|| panic!("missing ServerApi operation {operation_id}"));
|
||||||
|
assert_eq!(operation.method, method, "{operation_id}");
|
||||||
|
assert_eq!(operation.path, path, "{operation_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn worker_restore_state_round_trips_and_rejects_unknown_variants() {
|
fn worker_restore_state_round_trips_and_rejects_unknown_variants() {
|
||||||
for (state, wire) in [
|
for (state, wire) in [
|
||||||
@@ -5248,6 +5544,75 @@ mod openapi_artifact_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_and_workspace_catalog_contract_is_strict_and_excludes_cookie_routes() {
|
||||||
|
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
||||||
|
let value: serde_json::Value =
|
||||||
|
serde_json::from_str(&document.to_json().expect("document must serialize"))
|
||||||
|
.expect("document must be JSON");
|
||||||
|
|
||||||
|
for (path, method) in [
|
||||||
|
("/health", "get"),
|
||||||
|
("/api/auth/config", "get"),
|
||||||
|
("/api/auth/bootstrap-user", "post"),
|
||||||
|
("/api/auth/passkeys/registration/options", "post"),
|
||||||
|
("/api/auth/passkeys/login/options", "post"),
|
||||||
|
("/api/auth/device-login/start", "post"),
|
||||||
|
("/api/auth/device-login/approve", "post"),
|
||||||
|
("/api/auth/device-login/poll", "post"),
|
||||||
|
("/api/auth/whoami", "get"),
|
||||||
|
("/api/workspaces", "get"),
|
||||||
|
("/api/workspaces", "post"),
|
||||||
|
("/api/workspaces/{workspace_id}/deletion", "get"),
|
||||||
|
("/api/workspaces/{workspace_id}/deletion", "post"),
|
||||||
|
("/api/workspace-deletions/{operation_id}", "get"),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
value["paths"][path][method].is_object(),
|
||||||
|
"missing {method} {path}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let workspace_catalog = &value["paths"]["/api/workspaces"];
|
||||||
|
for method in ["get", "post"] {
|
||||||
|
assert_eq!(
|
||||||
|
workspace_catalog[method]["security"][0]["bearerAuth"],
|
||||||
|
serde_json::json!([])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workspace_catalog[method]["security"][1]["browserSession"],
|
||||||
|
serde_json::json!([])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for status in ["200", "201"] {
|
||||||
|
assert!(workspace_catalog["post"]["responses"][status].is_object());
|
||||||
|
}
|
||||||
|
let deletion_start = &value["paths"]["/api/workspaces/{workspace_id}/deletion"]["post"];
|
||||||
|
for status in ["200", "202", "409"] {
|
||||||
|
assert!(deletion_start["responses"][status].is_object());
|
||||||
|
}
|
||||||
|
for path in [
|
||||||
|
"/api/auth/passkeys/registration/complete",
|
||||||
|
"/api/auth/passkeys/login/complete",
|
||||||
|
"/api/auth/logout",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
value["paths"].get(path).is_none(),
|
||||||
|
"cookie response route {path} must remain outside ServerApi"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for schema in [
|
||||||
|
"RepositoryApiError",
|
||||||
|
"WorkspaceCreateRequest",
|
||||||
|
"WorkspaceDeletionRequest",
|
||||||
|
"DeviceLoginStartResponse",
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
value["components"]["schemas"][schema]["additionalProperties"], false,
|
||||||
|
"{schema} must reject unknown fields"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repository_operations_are_in_the_canonical_contract() {
|
fn repository_operations_are_in_the_canonical_contract() {
|
||||||
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
||||||
|
|||||||
@@ -1132,6 +1132,50 @@ impl From<&WorkspaceApi> for ServerAuthApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ServerApiContractService {
|
||||||
|
Server(WorkspaceServerApi),
|
||||||
|
Workspace(WorkspaceApi),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerApiContractService {
|
||||||
|
fn auth_api(&self) -> ServerAuthApi {
|
||||||
|
match self {
|
||||||
|
Self::Server(api) => ServerAuthApi {
|
||||||
|
config: api.template.as_ref().clone(),
|
||||||
|
store: api.store.clone(),
|
||||||
|
},
|
||||||
|
Self::Workspace(api) => ServerAuthApi::from(api),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_api(
|
||||||
|
&self,
|
||||||
|
) -> std::result::Result<&WorkspaceServerApi, server_api::RepositoryApiError> {
|
||||||
|
match self {
|
||||||
|
Self::Server(api) => Ok(api),
|
||||||
|
Self::Workspace(_) => Err(server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"Not Found",
|
||||||
|
"Server catalog operation is unavailable on a Workspace-local router",
|
||||||
|
Vec::new(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_api(&self) -> std::result::Result<&WorkspaceApi, server_api::RepositoryApiError> {
|
||||||
|
match self {
|
||||||
|
Self::Workspace(api) => Ok(api),
|
||||||
|
Self::Server(_) => Err(server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"Not Found",
|
||||||
|
"Workspace-local operation is unavailable on the Server catalog router",
|
||||||
|
Vec::new(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WorkerRemovalService {
|
struct WorkerRemovalService {
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
@@ -1990,173 +2034,6 @@ fn forbidden_server_response(message: &str) -> Response {
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct WorkspaceListQuery {
|
|
||||||
limit: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn server_health() -> Json<serde_json::Value> {
|
|
||||||
Json(serde_json::json!({ "status": "ok" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_server_workspaces(
|
|
||||||
State(api): State<WorkspaceServerApi>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Query(query): Query<WorkspaceListQuery>,
|
|
||||||
) -> Response {
|
|
||||||
let owner_account_id = match resolve_server_actor(&api, &headers).await {
|
|
||||||
Ok(Some(actor)) => actor.account_id,
|
|
||||||
Ok(None) => match api.catalog.is_empty() {
|
|
||||||
Ok(true) => {
|
|
||||||
return Json(WorkspaceCatalogListResponse(Vec::new())).into_response();
|
|
||||||
}
|
|
||||||
Ok(false) => return StatusCode::UNAUTHORIZED.into_response(),
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
},
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
match api
|
|
||||||
.catalog
|
|
||||||
.list(&owner_account_id, query.limit.unwrap_or(100))
|
|
||||||
{
|
|
||||||
Ok(workspaces) => Json(WorkspaceCatalogListResponse(
|
|
||||||
workspaces.into_iter().map(workspace_summary).collect(),
|
|
||||||
))
|
|
||||||
.into_response(),
|
|
||||||
Err(error) => server_error_response(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_server_workspace(
|
|
||||||
State(api): State<WorkspaceServerApi>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Json(request): Json<WorkspaceCreateRequest>,
|
|
||||||
) -> Response {
|
|
||||||
let owner_account_id = match resolve_server_actor(&api, &headers).await {
|
|
||||||
Ok(Some(actor)) => actor.account_id,
|
|
||||||
Ok(None) => {
|
|
||||||
return forbidden_server_response("Workspace creation requires an authenticated owner");
|
|
||||||
}
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
let created = match api.catalog.create(request, owner_account_id) {
|
|
||||||
Ok(created) => created,
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
if let Err(error) = api
|
|
||||||
.router_for_workspace(&created.workspace.workspace_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return server_error_response(error);
|
|
||||||
}
|
|
||||||
let status = if created.replayed {
|
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::CREATED
|
|
||||||
};
|
|
||||||
(status, Json(workspace_create_response(created))).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn preflight_server_workspace_deletion(
|
|
||||||
State(api): State<WorkspaceServerApi>,
|
|
||||||
AxumPath(workspace_id): AxumPath<String>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> Response {
|
|
||||||
let actor_account_id = match resolve_server_actor(&api, &headers).await {
|
|
||||||
Ok(Some(actor)) => actor.account_id,
|
|
||||||
Ok(None) => return forbidden_server_response("Workspace deletion requires its owner"),
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
match api
|
|
||||||
.workspace_deletion_preflight(&actor_account_id, &workspace_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(preflight) => Json(preflight).into_response(),
|
|
||||||
Err(error) => server_error_response(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_server_workspace_deletion(
|
|
||||||
State(api): State<WorkspaceServerApi>,
|
|
||||||
AxumPath(workspace_id): AxumPath<String>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Json(request): Json<WorkspaceDeletionRequest>,
|
|
||||||
) -> Response {
|
|
||||||
let actor_account_id = match resolve_server_actor(&api, &headers).await {
|
|
||||||
Ok(Some(actor)) => actor.account_id,
|
|
||||||
Ok(None) => return forbidden_server_response("Workspace deletion requires its owner"),
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
let mutation_lock = api.mutation_lock(&workspace_id).await;
|
|
||||||
let _mutation_guard = mutation_lock.lock().await;
|
|
||||||
let existing = match api
|
|
||||||
.store
|
|
||||||
.workspace_deletion_operation(&actor_account_id, &request.operation_id)
|
|
||||||
{
|
|
||||||
Ok(existing) => existing,
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
if existing.is_none() {
|
|
||||||
let preflight = match api
|
|
||||||
.workspace_deletion_preflight(&actor_account_id, &workspace_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(preflight) => preflight,
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
if !preflight.can_delete {
|
|
||||||
return (StatusCode::CONFLICT, Json(preflight)).into_response();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let reservation =
|
|
||||||
match api
|
|
||||||
.store
|
|
||||||
.reserve_workspace_deletion(&actor_account_id, &workspace_id, &request)
|
|
||||||
{
|
|
||||||
Ok(reservation) => reservation,
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
if let Some(handle) = api.hook_handles.lock().await.remove(&workspace_id) {
|
|
||||||
handle.abort();
|
|
||||||
}
|
|
||||||
let operation = reservation.operation;
|
|
||||||
if operation.state != WorkspaceDeletionState::Succeeded {
|
|
||||||
api.schedule_workspace_deletion(request.operation_id).await;
|
|
||||||
}
|
|
||||||
let status = if operation.state == WorkspaceDeletionState::Succeeded {
|
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::ACCEPTED
|
|
||||||
};
|
|
||||||
(status, Json(operation)).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_server_workspace_deletion(
|
|
||||||
State(api): State<WorkspaceServerApi>,
|
|
||||||
AxumPath(operation_id): AxumPath<String>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> Response {
|
|
||||||
let actor_account_id = match resolve_server_actor(&api, &headers).await {
|
|
||||||
Ok(Some(actor)) => actor.account_id,
|
|
||||||
Ok(None) => {
|
|
||||||
return forbidden_server_response("Workspace deletion status requires its owner");
|
|
||||||
}
|
|
||||||
Err(error) => return server_error_response(error),
|
|
||||||
};
|
|
||||||
match api
|
|
||||||
.store
|
|
||||||
.workspace_deletion_operation(&actor_account_id, &operation_id)
|
|
||||||
{
|
|
||||||
Ok(Some(operation)) => Json(operation).into_response(),
|
|
||||||
Ok(None) => (
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"Workspace deletion operation not found",
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
Err(error) => server_error_response(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_summary(record: WorkspaceRecord) -> WorkspaceSummary {
|
fn workspace_summary(record: WorkspaceRecord) -> WorkspaceSummary {
|
||||||
WorkspaceSummary {
|
WorkspaceSummary {
|
||||||
workspace_id: record.workspace_id,
|
workspace_id: record.workspace_id,
|
||||||
@@ -2666,22 +2543,17 @@ pub async fn build_workspace_server_router(
|
|||||||
let api = WorkspaceServerApi::new(template, store);
|
let api = WorkspaceServerApi::new(template, store);
|
||||||
api.preload().await?;
|
api.preload().await?;
|
||||||
api.recover_workspace_deletions().await?;
|
api.recover_workspace_deletions().await?;
|
||||||
|
let contract_service = ServerApiContractService::Server(api.clone());
|
||||||
let catalog = Router::new()
|
let catalog = Router::new()
|
||||||
.route("/health", get(server_health))
|
|
||||||
.route(
|
|
||||||
"/api/workspaces",
|
|
||||||
get(list_server_workspaces).post(create_server_workspace),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/workspaces/{workspace_id}/deletion",
|
|
||||||
get(preflight_server_workspace_deletion).post(start_server_workspace_deletion),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/workspace-deletions/{operation_id}",
|
|
||||||
get(get_server_workspace_deletion),
|
|
||||||
)
|
|
||||||
.fallback(dispatch_workspace_request)
|
.fallback(dispatch_workspace_request)
|
||||||
.with_state(api.clone());
|
.with_state(api.clone())
|
||||||
|
.merge(server_api::server_api_axum::health(Arc::new(
|
||||||
|
contract_service.clone(),
|
||||||
|
)))
|
||||||
|
.merge(generated_auth_contract_router(contract_service.clone()))
|
||||||
|
.merge(generated_workspace_catalog_contract_router(
|
||||||
|
contract_service,
|
||||||
|
));
|
||||||
Ok(auth
|
Ok(auth
|
||||||
.merge(catalog)
|
.merge(catalog)
|
||||||
.layer(axum::middleware::from_fn_with_state(
|
.layer(axum::middleware::from_fn_with_state(
|
||||||
@@ -3752,39 +3624,383 @@ fn configured_repository_from_record(record: RepositoryRecord) -> Result<Configu
|
|||||||
|
|
||||||
fn build_server_auth_router(api: ServerAuthApi) -> Router {
|
fn build_server_auth_router(api: ServerAuthApi) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/auth/config", get(get_auth_config))
|
|
||||||
.route("/api/auth/bootstrap-user", post(post_auth_bootstrap_user))
|
|
||||||
.route(
|
|
||||||
"/api/auth/passkeys/registration/options",
|
|
||||||
post(post_passkey_registration_options),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/passkeys/registration/complete",
|
"/api/auth/passkeys/registration/complete",
|
||||||
post(post_passkey_registration_complete),
|
post(post_passkey_registration_complete),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/auth/passkeys/login/options",
|
|
||||||
post(post_passkey_login_options),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/passkeys/login/complete",
|
"/api/auth/passkeys/login/complete",
|
||||||
post(post_passkey_login_complete),
|
post(post_passkey_login_complete),
|
||||||
)
|
)
|
||||||
.route("/api/auth/logout", post(post_auth_logout))
|
.route("/api/auth/logout", post(post_auth_logout))
|
||||||
.route(
|
|
||||||
"/api/auth/device-login/start",
|
|
||||||
post(post_device_login_start),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/auth/device-login/approve",
|
|
||||||
post(post_device_login_approve),
|
|
||||||
)
|
|
||||||
.route("/api/auth/device-login/poll", post(post_device_login_poll))
|
|
||||||
.route("/api/auth/whoami", get(get_auth_whoami))
|
|
||||||
.with_state(api)
|
.with_state(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl server_api::ServerApi for WorkspaceApi {
|
async fn attach_server_origin_context(mut request: Request, next: Next) -> Response {
|
||||||
|
let actor = request.extensions().get::<RequestActor>().cloned();
|
||||||
|
let origin = request_origin(request.headers());
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(server_api::ServerRequestContext { actor, origin });
|
||||||
|
next.run(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn attach_server_request_context(
|
||||||
|
State(service): State<ServerApiContractService>,
|
||||||
|
mut request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
let actor = if let Some(actor) = request.extensions().get::<RequestActor>().cloned() {
|
||||||
|
Some(actor)
|
||||||
|
} else {
|
||||||
|
let auth = service.auth_api();
|
||||||
|
match resolve_request_actor(
|
||||||
|
auth.store.as_ref(),
|
||||||
|
request.headers(),
|
||||||
|
&auth_public_config(&auth.config).cookie_name,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(actor) => actor,
|
||||||
|
Err(error) => return server_error_response(error),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let origin = request_origin(request.headers());
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(server_api::ServerRequestContext { actor, origin });
|
||||||
|
next.run(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_auth_contract_router(service: ServerApiContractService) -> Router {
|
||||||
|
let service = Arc::new(service);
|
||||||
|
let origin_context = Router::new()
|
||||||
|
.merge(server_api::server_api_axum::auth_passkey_registration_options(service.clone()))
|
||||||
|
.merge(server_api::server_api_axum::auth_passkey_login_options(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.layer(middleware::from_fn(attach_server_origin_context));
|
||||||
|
let actor_context = Router::new()
|
||||||
|
.merge(server_api::server_api_axum::auth_device_login_approve(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::auth_whoami(service.clone()))
|
||||||
|
.layer(middleware::from_fn_with_state(
|
||||||
|
service.as_ref().clone(),
|
||||||
|
attach_server_request_context,
|
||||||
|
));
|
||||||
|
Router::new()
|
||||||
|
.merge(server_api::server_api_axum::auth_config(service.clone()))
|
||||||
|
.merge(server_api::server_api_axum::auth_bootstrap_user(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::auth_device_login_start(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::auth_device_login_poll(service))
|
||||||
|
.merge(origin_context)
|
||||||
|
.merge(actor_context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_workspace_catalog_contract_router(service: ServerApiContractService) -> Router {
|
||||||
|
let service = Arc::new(service);
|
||||||
|
Router::new()
|
||||||
|
.merge(server_api::server_api_axum::workspace_catalog_list(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::workspace_catalog_create(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::workspace_deletion_preflight(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::workspace_deletion_start(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::workspace_deletion_get(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.layer(middleware::from_fn_with_state(
|
||||||
|
service.as_ref().clone(),
|
||||||
|
attach_server_request_context,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_workspace_contract_router(service: ServerApiContractService) -> Router {
|
||||||
|
let service = Arc::new(service);
|
||||||
|
Router::new()
|
||||||
|
.merge(server_api::server_api_axum::worker_session(service.clone()))
|
||||||
|
.merge(server_api::server_api_axum::repository_list(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::repository_list_alias(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::repository_detail(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::repository_detail_alias(
|
||||||
|
service.clone(),
|
||||||
|
))
|
||||||
|
.merge(server_api::server_api_axum::repository_create(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl server_api::ServerApi for ServerApiContractService {
|
||||||
|
async fn health(
|
||||||
|
&self,
|
||||||
|
) -> std::result::Result<server_api::HealthResponse, server_api::RepositoryApiError> {
|
||||||
|
Ok(server_api::HealthResponse {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_config(
|
||||||
|
&self,
|
||||||
|
) -> std::result::Result<AuthPublicConfig, server_api::RepositoryApiError> {
|
||||||
|
Ok(auth_public_config(&self.auth_api().config))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_bootstrap_user(
|
||||||
|
&self,
|
||||||
|
request: AuthBootstrapUserRequest,
|
||||||
|
) -> std::result::Result<AuthUserResponse, server_api::RepositoryApiError> {
|
||||||
|
post_auth_bootstrap_user(State(self.auth_api()), Json(request))
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_passkey_registration_options(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
mut request: PasskeyRegistrationOptionsRequest,
|
||||||
|
) -> std::result::Result<PasskeyRegistrationOptionsResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
if request.browser_origin.is_none() {
|
||||||
|
request.browser_origin = context.origin;
|
||||||
|
}
|
||||||
|
post_passkey_registration_options(State(self.auth_api()), HeaderMap::new(), Json(request))
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_passkey_login_options(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
mut request: PasskeyLoginOptionsRequest,
|
||||||
|
) -> std::result::Result<PasskeyLoginOptionsResponse, server_api::RepositoryApiError> {
|
||||||
|
if request.browser_origin.is_none() {
|
||||||
|
request.browser_origin = context.origin;
|
||||||
|
}
|
||||||
|
post_passkey_login_options(State(self.auth_api()), HeaderMap::new(), Json(request))
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_device_login_start(
|
||||||
|
&self,
|
||||||
|
request: DeviceLoginStartRequest,
|
||||||
|
) -> std::result::Result<DeviceLoginStartResponse, server_api::RepositoryApiError> {
|
||||||
|
post_device_login_start(State(self.auth_api()), Json(request))
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_device_login_approve(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
request: DeviceLoginApproveRequest,
|
||||||
|
) -> std::result::Result<DeviceLoginApproveResponse, server_api::RepositoryApiError> {
|
||||||
|
let actor = context.actor.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::UNAUTHORIZED.as_u16(),
|
||||||
|
"Unauthorized",
|
||||||
|
"request requires a browser session or Bearer API token",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
approve_device_login(&self.auth_api(), actor, request)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_device_login_poll(
|
||||||
|
&self,
|
||||||
|
request: DeviceLoginPollRequest,
|
||||||
|
) -> std::result::Result<DeviceLoginPollResponse, server_api::RepositoryApiError> {
|
||||||
|
post_device_login_poll(State(self.auth_api()), Json(request))
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_whoami(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
) -> std::result::Result<WhoamiResponse, server_api::RepositoryApiError> {
|
||||||
|
Ok(WhoamiResponse {
|
||||||
|
actor: context.actor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn workspace_catalog_list(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
query: server_api::WorkspaceListQuery,
|
||||||
|
) -> std::result::Result<WorkspaceCatalogListResponse, server_api::RepositoryApiError> {
|
||||||
|
let api = self.server_api()?;
|
||||||
|
let Some(actor) = context.actor else {
|
||||||
|
return if api
|
||||||
|
.catalog
|
||||||
|
.is_empty()
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?
|
||||||
|
{
|
||||||
|
Ok(WorkspaceCatalogListResponse(Vec::new()))
|
||||||
|
} else {
|
||||||
|
Err(server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::UNAUTHORIZED.as_u16(),
|
||||||
|
"Unauthorized",
|
||||||
|
"authentication required",
|
||||||
|
Vec::new(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let workspaces = api
|
||||||
|
.catalog
|
||||||
|
.list(
|
||||||
|
&actor.account_id,
|
||||||
|
query.limit.unwrap_or(100).try_into().unwrap_or(200),
|
||||||
|
)
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
Ok(WorkspaceCatalogListResponse(
|
||||||
|
workspaces.into_iter().map(workspace_summary).collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn workspace_catalog_create(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
request: WorkspaceCreateRequest,
|
||||||
|
) -> std::result::Result<WorkspaceCreateResponse, server_api::RepositoryApiError> {
|
||||||
|
let api = self.server_api()?;
|
||||||
|
let actor = context.actor.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::FORBIDDEN.as_u16(),
|
||||||
|
"Forbidden",
|
||||||
|
"Workspace creation requires an authenticated owner",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let created = api
|
||||||
|
.catalog
|
||||||
|
.create(request, actor.account_id)
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
api.router_for_workspace(&created.workspace.workspace_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
Ok(workspace_create_response(created))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn workspace_deletion_preflight(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
workspace_id: String,
|
||||||
|
) -> std::result::Result<WorkspaceDeletionPreflightResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
let api = self.server_api()?;
|
||||||
|
let actor = context.actor.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::FORBIDDEN.as_u16(),
|
||||||
|
"Forbidden",
|
||||||
|
"Workspace deletion requires its owner",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
api.workspace_deletion_preflight(&actor.account_id, &workspace_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn workspace_deletion_start(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
workspace_id: String,
|
||||||
|
request: WorkspaceDeletionRequest,
|
||||||
|
) -> std::result::Result<WorkspaceDeletionOperationResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
let api = self.server_api()?;
|
||||||
|
let actor = context.actor.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::FORBIDDEN.as_u16(),
|
||||||
|
"Forbidden",
|
||||||
|
"Workspace deletion requires its owner",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mutation_lock = api.mutation_lock(&workspace_id).await;
|
||||||
|
let _mutation_guard = mutation_lock.lock().await;
|
||||||
|
let existing = api
|
||||||
|
.store
|
||||||
|
.workspace_deletion_operation(&actor.account_id, &request.operation_id)
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
if existing.is_none() {
|
||||||
|
let preflight = api
|
||||||
|
.workspace_deletion_preflight(&actor.account_id, &workspace_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
if !preflight.can_delete {
|
||||||
|
return Err(server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::CONFLICT.as_u16(),
|
||||||
|
"Conflict",
|
||||||
|
"Workspace deletion preflight is blocked",
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reservation = api
|
||||||
|
.store
|
||||||
|
.reserve_workspace_deletion(&actor.account_id, &workspace_id, &request)
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?;
|
||||||
|
if let Some(handle) = api.hook_handles.lock().await.remove(&workspace_id) {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
let operation = reservation.operation;
|
||||||
|
if operation.state != WorkspaceDeletionState::Succeeded {
|
||||||
|
api.schedule_workspace_deletion(request.operation_id).await;
|
||||||
|
}
|
||||||
|
Ok(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn workspace_deletion_get(
|
||||||
|
&self,
|
||||||
|
context: server_api::ServerRequestContext,
|
||||||
|
operation_id: String,
|
||||||
|
) -> std::result::Result<WorkspaceDeletionOperationResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
let api = self.server_api()?;
|
||||||
|
let actor = context.actor.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::FORBIDDEN.as_u16(),
|
||||||
|
"Forbidden",
|
||||||
|
"Workspace deletion status requires its owner",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
api.store
|
||||||
|
.workspace_deletion_operation(&actor.account_id, &operation_id)
|
||||||
|
.map_err(|error| ApiError::from(error).into_repository_api_error())?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"Not Found",
|
||||||
|
"Workspace deletion operation not found",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn worker_session(
|
async fn worker_session(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
@@ -3792,7 +4008,14 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
worker_id: String,
|
worker_id: String,
|
||||||
) -> std::result::Result<server_api::WorkspaceWorkerSessionResponse, server_api::ServerApiError>
|
) -> std::result::Result<server_api::WorkspaceWorkerSessionResponse, server_api::ServerApiError>
|
||||||
{
|
{
|
||||||
if workspace_id != self.config.workspace_id {
|
let api = self.workspace_api().map_err(|_| {
|
||||||
|
runtime_api::RuntimeApiError::new(
|
||||||
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"workspace_not_found",
|
||||||
|
"Workspace was not found",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if workspace_id != api.config.workspace_id {
|
||||||
return Err(runtime_api::RuntimeApiError::new(
|
return Err(runtime_api::RuntimeApiError::new(
|
||||||
StatusCode::NOT_FOUND.as_u16(),
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
"workspace_not_found",
|
"workspace_not_found",
|
||||||
@@ -3800,7 +4023,7 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let worker = RuntimeWorkerRef::new(runtime_id.clone(), worker_id.clone());
|
let worker = RuntimeWorkerRef::new(runtime_id.clone(), worker_id.clone());
|
||||||
let observation = self
|
let observation = api
|
||||||
.runtime
|
.runtime
|
||||||
.worker_session(&workspace_id, &worker)
|
.worker_session(&workspace_id, &worker)
|
||||||
.map_err(|_error| {
|
.map_err(|_error| {
|
||||||
@@ -3824,10 +4047,8 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
) -> std::result::Result<server_api::RepositoryListResponse, server_api::RepositoryApiError>
|
) -> std::result::Result<server_api::RepositoryListResponse, server_api::RepositoryApiError>
|
||||||
{
|
{
|
||||||
scoped_list_repositories(
|
let api = self.workspace_api()?.clone();
|
||||||
State(self.clone()),
|
scoped_list_repositories(State(api), AxumPath(ScopedWorkspacePath { workspace_id }))
|
||||||
AxumPath(ScopedWorkspacePath { workspace_id }),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map(|Json(response)| response)
|
.map(|Json(response)| response)
|
||||||
.map_err(ApiError::into_repository_api_error)
|
.map_err(ApiError::into_repository_api_error)
|
||||||
@@ -3837,7 +4058,7 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
&self,
|
&self,
|
||||||
) -> std::result::Result<server_api::RepositoryListResponse, server_api::RepositoryApiError>
|
) -> std::result::Result<server_api::RepositoryListResponse, server_api::RepositoryApiError>
|
||||||
{
|
{
|
||||||
list_repositories(State(self.clone()))
|
list_repositories(State(self.workspace_api()?.clone()))
|
||||||
.await
|
.await
|
||||||
.map(|Json(response)| response)
|
.map(|Json(response)| response)
|
||||||
.map_err(ApiError::into_repository_api_error)
|
.map_err(ApiError::into_repository_api_error)
|
||||||
@@ -3849,8 +4070,9 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
repository_key: String,
|
repository_key: String,
|
||||||
) -> std::result::Result<server_api::RepositoryDetailResponse, server_api::RepositoryApiError>
|
) -> std::result::Result<server_api::RepositoryDetailResponse, server_api::RepositoryApiError>
|
||||||
{
|
{
|
||||||
|
let api = self.workspace_api()?.clone();
|
||||||
scoped_repository_detail(
|
scoped_repository_detail(
|
||||||
State(self.clone()),
|
State(api),
|
||||||
AxumPath(ScopedRepositoryPath {
|
AxumPath(ScopedRepositoryPath {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
repository_key,
|
repository_key,
|
||||||
@@ -3866,7 +4088,10 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
repository_key: String,
|
repository_key: String,
|
||||||
) -> std::result::Result<server_api::RepositoryDetailResponse, server_api::RepositoryApiError>
|
) -> std::result::Result<server_api::RepositoryDetailResponse, server_api::RepositoryApiError>
|
||||||
{
|
{
|
||||||
repository_detail(State(self.clone()), AxumPath(repository_key))
|
repository_detail(
|
||||||
|
State(self.workspace_api()?.clone()),
|
||||||
|
AxumPath(repository_key),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map(|Json(response)| response)
|
.map(|Json(response)| response)
|
||||||
.map_err(ApiError::into_repository_api_error)
|
.map_err(ApiError::into_repository_api_error)
|
||||||
@@ -3880,7 +4105,7 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
) -> std::result::Result<CreateWorkspaceRepositoryResponse, server_api::RepositoryApiError>
|
) -> std::result::Result<CreateWorkspaceRepositoryResponse, server_api::RepositoryApiError>
|
||||||
{
|
{
|
||||||
scoped_create_repository(
|
scoped_create_repository(
|
||||||
State(self.clone()),
|
State(self.workspace_api()?.clone()),
|
||||||
AxumPath(ScopedWorkspacePath { workspace_id }),
|
AxumPath(ScopedWorkspacePath { workspace_id }),
|
||||||
Extension(actor),
|
Extension(actor),
|
||||||
Json(request),
|
Json(request),
|
||||||
@@ -3892,7 +4117,9 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_inner_router(api: WorkspaceApi) -> Router {
|
fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||||
let auth = build_server_auth_router(ServerAuthApi::from(&api));
|
let contract_service = ServerApiContractService::Workspace(api.clone());
|
||||||
|
let auth = build_server_auth_router(ServerAuthApi::from(&api))
|
||||||
|
.merge(generated_auth_contract_router(contract_service.clone()));
|
||||||
let scoped_ticket_relations_query_path =
|
let scoped_ticket_relations_query_path =
|
||||||
format!("/api/w/{{workspace_id}}{TICKET_RELATIONS_QUERY_PATH}");
|
format!("/api/w/{{workspace_id}}{TICKET_RELATIONS_QUERY_PATH}");
|
||||||
let scoped_ticket_orchestration_plans_query_path =
|
let scoped_ticket_orchestration_plans_query_path =
|
||||||
@@ -4489,11 +4716,9 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
|||||||
"/api/w/{workspace_id}/hosts/{host_id}/workers",
|
"/api/w/{workspace_id}/hosts/{host_id}/workers",
|
||||||
get(scoped_list_host_workers),
|
get(scoped_list_host_workers),
|
||||||
)
|
)
|
||||||
.merge(
|
|
||||||
server_api::ServerApiAxum::router(api.clone()).with_state::<WorkspaceApi>(()),
|
|
||||||
)
|
|
||||||
.fallback(get(static_or_spa_fallback))
|
.fallback(get(static_or_spa_fallback))
|
||||||
.with_state(api);
|
.with_state(api)
|
||||||
|
.merge(generated_workspace_contract_router(contract_service));
|
||||||
auth.merge(workspace)
|
auth.merge(workspace)
|
||||||
.layer(middleware::from_fn(log_failed_api_response))
|
.layer(middleware::from_fn(log_failed_api_response))
|
||||||
}
|
}
|
||||||
@@ -15038,10 +15263,6 @@ async fn scoped_list_host_workers(
|
|||||||
list_host_workers(State(api), AxumPath(path.host_id)).await
|
list_host_workers(State(api), AxumPath(path.host_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_auth_config(State(api): State<ServerAuthApi>) -> ApiResult<Json<AuthPublicConfig>> {
|
|
||||||
Ok(Json(auth_public_config(&api.config)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post_auth_bootstrap_user(
|
async fn post_auth_bootstrap_user(
|
||||||
State(api): State<ServerAuthApi>,
|
State(api): State<ServerAuthApi>,
|
||||||
Json(request): Json<AuthBootstrapUserRequest>,
|
Json(request): Json<AuthBootstrapUserRequest>,
|
||||||
@@ -15387,12 +15608,11 @@ async fn post_device_login_start(
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_device_login_approve(
|
fn approve_device_login(
|
||||||
State(api): State<ServerAuthApi>,
|
api: &ServerAuthApi,
|
||||||
headers: HeaderMap,
|
actor: RequestActor,
|
||||||
Json(request): Json<DeviceLoginApproveRequest>,
|
request: DeviceLoginApproveRequest,
|
||||||
) -> ApiResult<Json<DeviceLoginApproveResponse>> {
|
) -> ApiResult<DeviceLoginApproveResponse> {
|
||||||
let actor = require_actor(&api, &headers).await?;
|
|
||||||
let flow = api
|
let flow = api
|
||||||
.store
|
.store
|
||||||
.get_device_login_flow_by_user_code(&request.user_code.trim().to_ascii_uppercase())?
|
.get_device_login_flow_by_user_code(&request.user_code.trim().to_ascii_uppercase())?
|
||||||
@@ -15441,10 +15661,10 @@ async fn post_device_login_approve(
|
|||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
Ok(Json(DeviceLoginApproveResponse {
|
Ok(DeviceLoginApproveResponse {
|
||||||
status: DeviceLoginApprovalStatus::Approved,
|
status: DeviceLoginApprovalStatus::Approved,
|
||||||
user: actor.user(),
|
user: actor.user(),
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_device_login_poll(
|
async fn post_device_login_poll(
|
||||||
@@ -15488,15 +15708,6 @@ async fn post_device_login_poll(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_auth_whoami(
|
|
||||||
State(api): State<ServerAuthApi>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> ApiResult<Json<WhoamiResponse>> {
|
|
||||||
Ok(Json(WhoamiResponse {
|
|
||||||
actor: resolve_actor(&api, &headers).await?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post_auth_logout(
|
async fn post_auth_logout(
|
||||||
State(api): State<ServerAuthApi>,
|
State(api): State<ServerAuthApi>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|||||||
+1679
-1
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||||
// Input: openapi/server-api.json
|
// Input: openapi/server-api.json
|
||||||
// Input contract: 0.1.0 (sha256:bac8ba9340cf541183e96715dd4ccd8367f965cdcfc398e6d5091f26a903e1ef)
|
// Input contract: 0.1.0 (sha256:b478a5e12b43d9ac1261ba31358a5b80bb31e398bd57eccbd797f5e92c6f48b7)
|
||||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||||
|
|||||||
Reference in New Issue
Block a user