feat: centralize auth REST contracts

This commit is contained in:
2026-09-03 17:35:24 +09:00
parent c1dfb1add5
commit bc810beb3b
11 changed files with 1553 additions and 431 deletions
+15 -35
View File
@@ -1,8 +1,11 @@
use crate::BackendOrigin;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use std::fmt;
use std::time::Duration;
use workspace_api::{DeviceLoginPollRequest, DeviceLoginPollStatus, DeviceLoginStartRequest};
pub use workspace_api::{DeviceLoginPollResponse, DeviceLoginStartResponse};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendAuthTarget {
pub base_url: String,
@@ -28,23 +31,6 @@ impl BackendAuthTarget {
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct DeviceLoginStartResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct DeviceLoginPollResponse {
pub status: String,
pub access_token: Option<String>,
pub token_type: Option<String>,
}
#[derive(Debug)]
pub enum BackendAuthClientError {
Http(reqwest::Error),
@@ -74,16 +60,6 @@ impl From<reqwest::Error> for BackendAuthClientError {
}
}
#[derive(Debug, Serialize)]
struct DeviceLoginStartRequest<'a> {
client_name: Option<&'a str>,
}
#[derive(Debug, Serialize)]
struct DeviceLoginPollRequest<'a> {
device_code: &'a str,
}
pub async fn start_device_login(
target: &BackendAuthTarget,
client_name: Option<&str>,
@@ -91,7 +67,9 @@ pub async fn start_device_login(
let client = reqwest::Client::new();
let response = client
.post(target.api_url("/api/auth/device-login/start"))
.json(&DeviceLoginStartRequest { client_name })
.json(&DeviceLoginStartRequest {
client_name: client_name.map(ToOwned::to_owned),
})
.send()
.await?;
parse_json_response(response).await
@@ -104,7 +82,9 @@ pub async fn poll_device_login(
let client = reqwest::Client::new();
let response = client
.post(target.api_url("/api/auth/device-login/poll"))
.json(&DeviceLoginPollRequest { device_code })
.json(&DeviceLoginPollRequest {
device_code: device_code.to_string(),
})
.send()
.await?;
parse_json_response(response).await
@@ -119,25 +99,25 @@ pub async fn wait_for_device_login(
let started = std::time::Instant::now();
loop {
let response = poll_device_login(target, device_code).await?;
match response.status.as_str() {
"approved" => {
match response.status {
DeviceLoginPollStatus::Approved => {
return response
.access_token
.ok_or(BackendAuthClientError::MissingAccessToken);
}
"expired" => {
DeviceLoginPollStatus::Expired => {
return Err(BackendAuthClientError::BackendStatus {
status: 410,
body: "device login expired".to_string(),
});
}
"consumed" => {
DeviceLoginPollStatus::Consumed => {
return Err(BackendAuthClientError::BackendStatus {
status: 409,
body: "device login was already consumed".to_string(),
});
}
_ => {}
DeviceLoginPollStatus::Pending => {}
}
if started.elapsed() >= expires_in {
return Err(BackendAuthClientError::BackendStatus {
+5
View File
@@ -13,6 +13,7 @@ typescript = ["dep:ts-rs", "protocol/typescript"]
protocol.workspace = true
serde = { workspace = true, features = ["derive"] }
ts-rs = { version = "12.0.1", optional = true }
webauthn-rs-proto = "0.5.5"
[[example]]
name = "generate_typescript"
@@ -37,6 +38,10 @@ required-features = ["typescript"]
name = "generate_memory_api_types"
required-features = ["typescript"]
[[example]]
name = "generate_auth_api_types"
required-features = ["typescript"]
[[example]]
name = "generate_repository_access_types"
required-features = ["typescript"]
@@ -0,0 +1,3 @@
fn main() {
print!("{}", workspace_api::auth_api_typescript());
}
+472
View File
@@ -5,6 +5,312 @@
//! callers must explicitly construct these Workspace-authoritative resources.
use serde::{Deserialize, Serialize};
use webauthn_rs_proto::{
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
RequestChallengeResponse,
};
/// Public browser-authentication configuration.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct AuthPublicConfig {
pub rp_id: String,
pub origin: String,
pub public_base_url: String,
pub cookie_name: String,
}
/// Authentication method that established the current request actor.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum ActorAuthMethod {
BrowserSession,
ApiToken,
}
/// Public user identity returned by authentication operations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct AuthenticatedUser {
pub user_id: String,
pub account_id: String,
pub handle: String,
pub display_name: String,
}
/// Authenticated actor returned by `GET /api/auth/whoami`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RequestActor {
pub user_id: String,
pub account_id: String,
pub handle: String,
pub display_name: String,
pub auth_method: ActorAuthMethod,
}
impl RequestActor {
pub fn user(&self) -> AuthenticatedUser {
AuthenticatedUser {
user_id: self.user_id.clone(),
account_id: self.account_id.clone(),
handle: self.handle.clone(),
display_name: self.display_name.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WhoamiResponse {
pub actor: Option<RequestActor>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct AuthBootstrapUserRequest {
pub handle: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub display_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct AuthUserResponse {
pub user: AuthenticatedUser,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyRegistrationOptionsRequest {
pub handle: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyRegistrationOptionsResponse {
pub challenge_id: String,
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
pub public_key: CreationChallengeResponse,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyRegistrationCompleteRequest {
pub challenge_id: String,
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
pub credential: RegisterPublicKeyCredential,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyLoginOptionsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub handle: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyLoginOptionsResponse {
pub challenge_id: String,
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
pub public_key: RequestChallengeResponse,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PasskeyLoginCompleteRequest {
pub challenge_id: String,
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
pub credential: PublicKeyCredential,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeviceLoginStartRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub client_name: Option<String>,
}
const DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS: u64 = 24 * 60 * 60;
const DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS: u64 = 60;
#[derive(Clone, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct DeviceLoginStartResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub expires_in: u64,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub interval: u64,
}
impl<'de> Deserialize<'de> for DeviceLoginStartResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
device_code: String,
user_code: String,
verification_uri: String,
verification_uri_complete: String,
expires_in: u64,
interval: u64,
}
let wire = Wire::deserialize(deserializer)?;
if wire.expires_in == 0 || wire.expires_in > DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS {
return Err(serde::de::Error::custom(format!(
"expires_in must be between 1 and {DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS}"
)));
}
if wire.interval == 0 || wire.interval > DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS {
return Err(serde::de::Error::custom(format!(
"interval must be between 1 and {DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS}"
)));
}
Ok(Self {
device_code: wire.device_code,
user_code: wire.user_code,
verification_uri: wire.verification_uri,
verification_uri_complete: wire.verification_uri_complete,
expires_in: wire.expires_in,
interval: wire.interval,
})
}
}
impl std::fmt::Debug for DeviceLoginStartResponse {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DeviceLoginStartResponse")
.field("device_code", &"[redacted]")
.field("user_code", &self.user_code)
.field("verification_uri", &self.verification_uri)
.field("verification_uri_complete", &self.verification_uri_complete)
.field("expires_in", &self.expires_in)
.field("interval", &self.interval)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeviceLoginApproveRequest {
pub user_code: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DeviceLoginApprovalStatus {
Approved,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeviceLoginApproveResponse {
pub status: DeviceLoginApprovalStatus,
pub user: AuthenticatedUser,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeviceLoginPollRequest {
pub device_code: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub enum DeviceAccessTokenType {
Bearer,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DeviceLoginPollStatus {
Pending,
Approved,
Expired,
Consumed,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeviceLoginPollResponse {
pub status: DeviceLoginPollStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub access_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub token_type: Option<DeviceAccessTokenType>,
}
impl std::fmt::Debug for DeviceLoginPollResponse {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DeviceLoginPollResponse")
.field("status", &self.status)
.field(
"access_token",
&self.access_token.as_ref().map(|_| "[redacted]"),
)
.field("token_type", &self.token_type)
.finish()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum LogoutStatus {
LoggedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct LogoutResponse {
pub status: LogoutStatus,
}
pub const REPOSITORY_KEY_MIN_LEN: usize = 1;
pub const REPOSITORY_KEY_MAX_LEN: usize = 64;
@@ -1698,6 +2004,47 @@ pub fn repository_access_api_typescript() -> String {
)
}
#[cfg(feature = "typescript")]
pub fn auth_api_typescript() -> String {
use ts_rs::TS;
let config = ts_rs::Config::default();
let declarations = [
AuthPublicConfig::decl(&config),
ActorAuthMethod::decl(&config),
AuthenticatedUser::decl(&config),
RequestActor::decl(&config),
WhoamiResponse::decl(&config),
AuthBootstrapUserRequest::decl(&config),
AuthUserResponse::decl(&config),
PasskeyRegistrationOptionsRequest::decl(&config),
PasskeyRegistrationOptionsResponse::decl(&config),
PasskeyRegistrationCompleteRequest::decl(&config),
PasskeyLoginOptionsRequest::decl(&config),
PasskeyLoginOptionsResponse::decl(&config),
PasskeyLoginCompleteRequest::decl(&config),
DeviceLoginStartRequest::decl(&config),
DeviceLoginStartResponse::decl(&config),
DeviceLoginApproveRequest::decl(&config),
DeviceLoginApprovalStatus::decl(&config),
DeviceLoginApproveResponse::decl(&config),
DeviceLoginPollRequest::decl(&config),
DeviceAccessTokenType::decl(&config),
DeviceLoginPollStatus::decl(&config),
DeviceLoginPollResponse::decl(&config),
LogoutStatus::decl(&config),
LogoutResponse::decl(&config),
];
format!(
"// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts\n\n{}\n",
declarations
.into_iter()
.map(|declaration| format!("export {declaration}"))
.collect::<Vec<_>>()
.join("\n\n")
)
}
#[cfg(feature = "typescript")]
pub fn workdir_api_typescript() -> String {
use ts_rs::TS;
@@ -2289,6 +2636,130 @@ mod tests {
}
}
#[test]
fn auth_server_fixtures_round_trip_through_shared_dtos() {
let whoami = serde_json::json!({
"actor": {
"user_id": "user-1",
"account_id": "account-1",
"handle": "hare",
"display_name": "Hare",
"auth_method": "browser_session"
}
});
let decoded = serde_json::from_value::<WhoamiResponse>(whoami.clone())
.expect("server whoami fixture should match shared DTO");
assert_eq!(serde_json::to_value(decoded).unwrap(), whoami);
let registration_options = serde_json::json!({
"challenge_id": "challenge-1",
"public_key": {
"publicKey": {
"challenge": "AQID",
"rp": {"id": "localhost", "name": "Yoi"},
"user": {"id": "BAUG", "name": "hare", "displayName": "Hare"},
"pubKeyCredParams": [{"type": "public-key", "alg": -7}]
}
}
});
let decoded = serde_json::from_value::<PasskeyRegistrationOptionsResponse>(
registration_options.clone(),
)
.expect("server registration options fixture should match shared DTO");
assert_eq!(serde_json::to_value(decoded).unwrap(), registration_options);
let device_start = serde_json::json!({
"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": 2
});
let decoded = serde_json::from_value::<DeviceLoginStartResponse>(device_start.clone())
.expect("server device-login fixture should match shared DTO");
assert_eq!(serde_json::to_value(decoded).unwrap(), device_start);
}
#[test]
fn auth_dtos_reject_unknown_status_and_fields() {
let unknown_status = serde_json::json!({"status": "future_status"});
assert!(
serde_json::from_value::<DeviceLoginPollResponse>(unknown_status).is_err(),
"unknown device-login statuses must fail closed"
);
let unsafe_expiry = serde_json::json!({
"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": 0,
"interval": 2
});
assert!(serde_json::from_value::<DeviceLoginStartResponse>(unsafe_expiry).is_err());
let malformed_credential = serde_json::json!({
"challenge_id": "challenge-1",
"credential": {
"id": "AQID",
"rawId": "AQID",
"type": "public-key",
"response": {"clientDataJSON": 42}
}
});
assert!(
serde_json::from_value::<PasskeyLoginCompleteRequest>(malformed_credential).is_err(),
"malformed passkey credential payloads must fail closed"
);
let unexpected_field = serde_json::json!({
"actor": null,
"access_token": "must-not-be-accepted"
});
assert!(serde_json::from_value::<WhoamiResponse>(unexpected_field).is_err());
}
#[test]
fn device_login_debug_output_redacts_secret_material() {
let start = DeviceLoginStartResponse {
device_code: "device-secret".to_string(),
user_code: "ABCD-EFGH".to_string(),
verification_uri: "https://yoi.example/login/device".to_string(),
verification_uri_complete: "https://yoi.example/login/device?user_code=ABCD-EFGH"
.to_string(),
expires_in: 600,
interval: 2,
};
let start_debug = format!("{start:?}");
assert!(!start_debug.contains("device-secret"));
assert!(start_debug.contains("[redacted]"));
let poll = DeviceLoginPollResponse {
status: DeviceLoginPollStatus::Approved,
access_token: Some("access-secret".to_string()),
token_type: Some(DeviceAccessTokenType::Bearer),
};
let poll_debug = format!("{poll:?}");
assert!(!poll_debug.contains("access-secret"));
assert!(poll_debug.contains("[redacted]"));
}
#[cfg(feature = "typescript")]
#[test]
fn generated_auth_api_contract_is_current() {
let expected = auth_api_typescript();
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/generated/auth-api.ts");
let actual = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_eq!(
normalize_typescript(&actual),
normalize_typescript(&expected),
"regenerate auth API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts` and format the generated file",
);
}
#[test]
fn companion_transcript_fixture_round_trips() {
round_trip(CompanionTranscriptProjection {
@@ -2383,6 +2854,7 @@ mod tests {
})
.collect::<String>()
.replace(";}", "}")
.replace("=|", "=")
}
#[test]
+1 -44
View File
@@ -1,54 +1,11 @@
use axum::http::{HeaderMap, header};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub use workspace_api::{ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor};
use crate::{Error, Result, store::ControlPlaneStore};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthPublicConfig {
pub rp_id: String,
pub origin: String,
pub public_base_url: String,
pub cookie_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RequestActor {
pub user_id: String,
pub account_id: String,
pub handle: String,
pub display_name: String,
pub auth_method: ActorAuthMethod,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ActorAuthMethod {
BrowserSession,
ApiToken,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthenticatedUser {
pub user_id: String,
pub account_id: String,
pub handle: String,
pub display_name: String,
}
impl RequestActor {
pub fn user(&self) -> AuthenticatedUser {
AuthenticatedUser {
user_id: self.user_id.clone(),
account_id: self.account_id.clone(),
handle: self.handle.clone(),
display_name: self.display_name.clone(),
}
}
}
pub fn normalize_handle(handle: &str) -> Result<String> {
let normalized = handle.trim().to_ascii_lowercase();
let valid = !normalized.is_empty()
+28 -135
View File
@@ -41,9 +41,7 @@ use tower::ServiceExt;
use url::Url;
use uuid::Uuid;
use webauthn_rs::prelude::{
CreationChallengeResponse, Passkey, PasskeyAuthentication, PasskeyRegistration,
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, Webauthn,
WebauthnBuilder,
Passkey, PasskeyAuthentication, PasskeyRegistration, Webauthn, WebauthnBuilder,
};
use workdir::http::{
WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError,
@@ -58,19 +56,26 @@ use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjec
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use workspace_api::{
BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, CreateRemoteRuntimeRequest,
CreateRepositorySshCredentialRequest, CreateWorkspaceRepositoryRequest,
CreateWorkspaceRepositoryResponse, CreateWorkspaceWorkerRequest,
CreateWorkspaceWorkerTicketAssignmentRequest, DeleteRepositorySshCredentialRequest,
DeleteRepositorySshHostTrustRequest, MemoryDocumentResponse, MemoryStagingListResponse,
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
ObjectiveStateRequest, ProfileSettingsResponse, PutRepositorySshHostTrustRequest,
ActorAuthMethod, AuthBootstrapUserRequest, AuthPublicConfig, AuthUserResponse,
AuthenticatedUser, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
CreateRemoteRuntimeRequest, CreateRepositorySshCredentialRequest,
CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse,
CreateWorkspaceWorkerRequest, CreateWorkspaceWorkerTicketAssignmentRequest,
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest,
DeviceAccessTokenType, DeviceLoginApprovalStatus, DeviceLoginApproveRequest,
DeviceLoginApproveResponse, DeviceLoginPollRequest, DeviceLoginPollResponse,
DeviceLoginPollStatus, DeviceLoginStartRequest, DeviceLoginStartResponse, LogoutResponse,
LogoutStatus, MemoryDocumentResponse, MemoryStagingListResponse, ObjectiveCreateRequest,
ObjectiveEditRequest, ObjectiveLinkTicketRequest, ObjectiveStateRequest,
PasskeyLoginCompleteRequest, PasskeyLoginOptionsRequest, PasskeyLoginOptionsResponse,
PasskeyRegistrationCompleteRequest, PasskeyRegistrationOptionsRequest,
PasskeyRegistrationOptionsResponse, ProfileSettingsResponse, PutRepositorySshHostTrustRequest,
RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse,
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust,
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, RequestActor,
RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary,
TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
UpdateWorkspaceMetadataRequest, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
UpdateWorkspaceMetadataRequest, WhoamiResponse, WorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
@@ -85,9 +90,9 @@ use workspace_api::{
};
use crate::auth::{
ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor, SessionCookiePolicy,
auth_error, is_expired, mint_secret, new_id, new_user_code, normalize_handle, parse_cookie,
resolve_request_actor, rfc3339_after, session_set_cookie, token_hash,
SessionCookiePolicy, auth_error, is_expired, mint_secret, new_id, new_user_code,
normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after, session_set_cookie,
token_hash,
};
use crate::authority::{
MemoryAuthority, ObjectiveAuthority, ObjectiveCreateInput, ObjectiveEditInput,
@@ -11477,118 +11482,6 @@ async fn scoped_list_host_workers(
list_host_workers(State(api), AxumPath(path.host_id)).await
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AuthBootstrapUserRequest {
handle: String,
#[serde(default)]
display_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct AuthUserResponse {
user: AuthenticatedUser,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyRegistrationOptionsRequest {
handle: String,
#[serde(default)]
display_name: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PasskeyRegistrationOptionsResponse {
challenge_id: String,
public_key: CreationChallengeResponse,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyRegistrationCompleteRequest {
challenge_id: String,
credential: RegisterPublicKeyCredential,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyLoginOptionsRequest {
#[serde(default)]
handle: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PasskeyLoginOptionsResponse {
challenge_id: String,
public_key: RequestChallengeResponse,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyLoginCompleteRequest {
challenge_id: String,
credential: PublicKeyCredential,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginStartRequest {
#[serde(default)]
client_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginStartResponse {
device_code: String,
user_code: String,
verification_uri: String,
verification_uri_complete: String,
expires_in: u64,
interval: u64,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginApproveRequest {
user_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginApproveResponse {
status: String,
user: AuthenticatedUser,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginPollRequest {
device_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginPollResponse {
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
access_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct WhoamiResponse {
actor: Option<RequestActor>,
}
#[derive(Debug, Serialize, Deserialize)]
struct LogoutResponse {
status: String,
}
async fn get_auth_config(State(api): State<ServerAuthApi>) -> ApiResult<Json<AuthPublicConfig>> {
Ok(Json(auth_public_config(&api.config)))
}
@@ -11993,7 +11886,7 @@ async fn post_device_login_approve(
.into());
}
Ok(Json(DeviceLoginApproveResponse {
status: "approved".to_string(),
status: DeviceLoginApprovalStatus::Approved,
user: actor.user(),
}))
}
@@ -12010,14 +11903,14 @@ async fn post_device_login_poll(
};
if is_expired(&flow.expires_at) {
return Ok(Json(DeviceLoginPollResponse {
status: "expired".to_string(),
status: DeviceLoginPollStatus::Expired,
access_token: None,
token_type: None,
}));
}
if flow.approved_at.is_none() {
return Ok(Json(DeviceLoginPollResponse {
status: "pending".to_string(),
status: DeviceLoginPollStatus::Pending,
access_token: None,
token_type: None,
}));
@@ -12027,15 +11920,15 @@ async fn post_device_login_poll(
.consume_device_login_token(&request.device_code, &crate::auth::now_rfc3339())?
else {
return Ok(Json(DeviceLoginPollResponse {
status: "consumed".to_string(),
status: DeviceLoginPollStatus::Consumed,
access_token: None,
token_type: None,
}));
};
Ok(Json(DeviceLoginPollResponse {
status: "approved".to_string(),
status: DeviceLoginPollStatus::Approved,
access_token: consumed.issued_access_token,
token_type: Some("Bearer".to_string()),
token_type: Some(DeviceAccessTokenType::Bearer),
}))
}
@@ -12073,7 +11966,7 @@ async fn post_auth_logout(
Ok((
response_headers,
Json(LogoutResponse {
status: "logged_out".to_string(),
status: LogoutStatus::LoggedOut,
}),
)
.into_response())