fix: type runtime config and test passkey lifecycle

This commit is contained in:
2026-09-23 20:15:23 +09:00
parent 237ab0e592
commit ed99be3e24
12 changed files with 1210 additions and 25 deletions
Generated
+78
View File
@@ -255,6 +255,28 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -4014,6 +4036,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_cbor_2"
version = "0.13.0"
@@ -4847,6 +4879,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
@@ -5571,6 +5615,39 @@ dependencies = [
"uuid",
]
[[package]]
name = "webauthn-authenticator-rs"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779e9c80ff248c7e12ea967f909249101f6e86f70fccd742d4b66c490c1710b4"
dependencies = [
"async-stream",
"async-trait",
"base64 0.21.7",
"base64urlsafedata",
"bitflags 1.3.2",
"futures",
"hex",
"nom 7.1.3",
"num-derive",
"num-traits",
"openssl",
"openssl-sys",
"serde",
"serde_bytes",
"serde_cbor_2",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tokio-stream",
"tracing",
"unicode-normalization",
"url",
"uuid",
"webauthn-rs-core",
"webauthn-rs-proto",
]
[[package]]
name = "webauthn-rs"
version = "0.5.5"
@@ -6338,6 +6415,7 @@ dependencies = [
"ts-rs",
"url",
"uuid",
"webauthn-authenticator-rs",
"webauthn-rs",
"workdir",
"worker",
+260 -4
View File
@@ -3157,10 +3157,171 @@ pub struct WorkspaceRuntimeConfigQuery {
pub profile: String,
}
/// Transparent JSON body for the Runtime-owned config bundle wire format.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(transparent)]
pub struct WorkspaceRuntimeConfigResponse(pub serde_json::Value);
/// Canonical Runtime config-bundle response projected by Workspace authority.
///
/// This is intentionally a concrete transport DTO rather than an opaque JSON value so generated
/// clients and OpenAPI consumers receive the same complete wire contract as the Runtime.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigResponse {
pub metadata: WorkspaceRuntimeConfigMetadata,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub profiles: Vec<WorkspaceRuntimeConfigProfile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub declarations: Vec<WorkspaceRuntimeConfigDeclaration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_catalog: Option<WorkspaceRuntimePromptCatalog>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive: Option<WorkspaceRuntimeProfileSourceArchive>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive_handle: Option<WorkspaceRuntimeResourceHandle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigMetadata {
pub id: String,
pub digest: String,
pub revision: String,
pub workspace_id: String,
pub created_at: String,
pub provenance: WorkspaceRuntimeConfigProvenance,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigProvenance {
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigProfile {
pub selector: WorkspaceRuntimeProfileSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum WorkspaceRuntimeProfileSelector {
Builtin(String),
Named(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigDeclaration {
pub kind: WorkspaceRuntimeConfigDeclarationKind,
pub name: String,
pub reference: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeConfigDeclarationKind {
SecretRef,
MountGrant,
NetworkPolicy,
ShellPolicy,
GitPolicy,
CapabilityGrant,
Unsupported,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimePromptCatalog {
pub templates: BTreeMap<String, String>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub config_revision: u64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_digest: String,
pub schema_fingerprint: String,
pub toolchain_fingerprint: String,
pub catalog_digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceArchive {
pub reference: WorkspaceRuntimeProfileSourceArchiveRef,
pub content: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceArchiveRef {
pub id: String,
pub digest: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub size_bytes: u64,
pub source_graph: WorkspaceRuntimeProfileSourceGraph,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceGraph {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub source_count: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub total_source_bytes: u64,
pub entrypoints: BTreeMap<String, String>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub import_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeResourceHandle {
pub kind: WorkspaceRuntimeResourceKind,
pub workspace_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
pub resource_id: String,
pub digest: String,
pub operation: WorkspaceRuntimeResourceOperation,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_i64))]
pub expires_at_unix_seconds: i64,
pub nonce: String,
pub revision: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub generation: Option<u64>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub max_bytes: u64,
pub content_type: String,
pub redaction: WorkspaceRuntimeResourceRedactionPolicy,
pub audit_correlation_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_graph: Option<WorkspaceRuntimeProfileSourceGraph>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceKind {
ProfileSourceArchive,
RepositorySshAccess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceOperation {
FetchArchive,
FetchOnce,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceRedactionPolicy {
RuntimeInternalOnly,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
@@ -8962,6 +9123,89 @@ mod skill_typescript_tests {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_config_response_round_trips_the_complete_config_bundle_shape() {
let fixture = serde_json::json!({
"metadata": {
"id": "bundle-a",
"digest": "0123456789abcdef",
"revision": "revision-a",
"workspace_id": "workspace-a",
"created_at": "2026-01-01T00:00:00Z",
"provenance": {
"source": "workspace_config",
"detail": "projected"
}
},
"profiles": [{
"selector": { "kind": "builtin", "value": "builtin:companion" },
"label": "Companion"
}],
"declarations": [{
"kind": "secret_ref",
"name": "github-token",
"reference": "secret:github-token"
}],
"prompt_catalog": {
"templates": { "default": "Prompt" },
"config_revision": 7,
"source_digest": "source-digest",
"schema_fingerprint": "schema-a",
"toolchain_fingerprint": "toolchain-a",
"catalog_digest": "catalog-a"
},
"profile_source_archive": {
"reference": {
"id": "archive-a",
"digest": "archive-digest",
"size_bytes": 3,
"source_graph": {
"source_count": 1,
"total_source_bytes": 3,
"entrypoints": { "builtin:companion": "companion.dcdl" },
"import_count": 0
}
},
"content": [1, 2, 3]
},
"profile_source_archive_handle": {
"kind": "profile_source_archive",
"workspace_id": "workspace-a",
"scope_id": "scope-a",
"runtime_id": "runtime-a",
"worker_id": "worker-a",
"resource_id": "resource-a",
"digest": "resource-digest",
"operation": "fetch_archive",
"expires_at_unix_seconds": 1_800_000_000,
"nonce": "nonce-a",
"revision": "revision-a",
"generation": 2,
"max_bytes": 1_048_576,
"content_type": "application/vnd.yoi.profile-source-archive+tar",
"redaction": "runtime_internal_only",
"audit_correlation_id": "audit-a",
"profile_source_graph": {
"source_count": 1,
"total_source_bytes": 3,
"entrypoints": { "builtin:companion": "companion.dcdl" },
"import_count": 0
}
}
});
let response: WorkspaceRuntimeConfigResponse =
serde_json::from_value(fixture.clone()).unwrap();
assert_eq!(serde_json::to_value(response).unwrap(), fixture);
let mut unknown_nested_field = fixture;
unknown_nested_field["metadata"]["runtime_local_path"] =
serde_json::Value::String("/private/path".to_string());
assert!(
serde_json::from_value::<WorkspaceRuntimeConfigResponse>(unknown_nested_field).is_err()
);
}
#[test]
fn worker_session_contract_and_flattened_availability_are_stable() {
let operations = ServerApiMetadata::OPERATIONS;
@@ -10896,6 +11140,18 @@ mod openapi_artifact_tests {
runtime_config["responses"]["200"]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/WorkspaceRuntimeConfigResponse"
);
let runtime_config_schema =
&value["components"]["schemas"]["WorkspaceRuntimeConfigResponse"];
assert_eq!(runtime_config_schema["type"], "object");
assert_eq!(runtime_config_schema["additionalProperties"], false);
assert!(runtime_config_schema["properties"]["metadata"].is_object());
assert!(runtime_config_schema["properties"]["profiles"].is_object());
assert!(runtime_config_schema["properties"]["declarations"].is_object());
assert!(
value["components"]["schemas"]["WorkspaceRuntimeConfigMetadata"]["properties"]
["digest"]
.is_object()
);
assert!(runtime_config["responses"]["304"].get("content").is_none());
for status in ["200", "304"] {
assert!(runtime_config["responses"][status]["headers"]["etag"].is_object());
+180 -2
View File
@@ -1,10 +1,13 @@
use crate::catalog::{ConfigBundleRef, ProfileSelector};
use crate::error::RuntimeError;
use crate::profile_archive::{
ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef,
ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef, ProfileSourceGraphSummary,
VerifiedProfileSourceArchive,
};
use crate::resource::{BackendResourceHandle, validate_resource_handle_text};
use crate::resource::{
BackendResourceHandle, BackendResourceKind, BackendResourceOperation, ResourceRedactionPolicy,
validate_resource_handle_text,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -162,6 +165,181 @@ impl ConfigBundle {
}
}
impl From<ConfigBundle> for server_api::WorkspaceRuntimeConfigResponse {
fn from(bundle: ConfigBundle) -> Self {
let ConfigBundle {
metadata,
profiles,
declarations,
prompt_catalog,
profile_source_archive,
profile_source_archive_handle,
} = bundle;
Self {
metadata: metadata.into(),
profiles: profiles.into_iter().map(Into::into).collect(),
declarations: declarations.into_iter().map(Into::into).collect(),
prompt_catalog: prompt_catalog.map(|catalog| {
server_api::WorkspaceRuntimePromptCatalog {
templates: catalog.templates,
config_revision: catalog.config_revision,
source_digest: catalog.source_digest,
schema_fingerprint: catalog.schema_fingerprint,
toolchain_fingerprint: catalog.toolchain_fingerprint,
catalog_digest: catalog.catalog_digest,
}
}),
profile_source_archive: profile_source_archive.map(Into::into),
profile_source_archive_handle: profile_source_archive_handle.map(Into::into),
}
}
}
impl From<ConfigBundleMetadata> for server_api::WorkspaceRuntimeConfigMetadata {
fn from(metadata: ConfigBundleMetadata) -> Self {
Self {
id: metadata.id,
digest: metadata.digest,
revision: metadata.revision,
workspace_id: metadata.workspace_id,
created_at: metadata.created_at,
provenance: metadata.provenance.into(),
}
}
}
impl From<ConfigBundleProvenance> for server_api::WorkspaceRuntimeConfigProvenance {
fn from(provenance: ConfigBundleProvenance) -> Self {
Self {
source: provenance.source,
detail: provenance.detail,
}
}
}
impl From<ConfigProfileDescriptor> for server_api::WorkspaceRuntimeConfigProfile {
fn from(profile: ConfigProfileDescriptor) -> Self {
Self {
selector: profile.selector.into(),
label: profile.label,
}
}
}
impl From<ProfileSelector> for server_api::WorkspaceRuntimeProfileSelector {
fn from(selector: ProfileSelector) -> Self {
match selector {
ProfileSelector::Builtin(value) => Self::Builtin(value),
ProfileSelector::Named(value) => Self::Named(value),
}
}
}
impl From<ConfigDeclaration> for server_api::WorkspaceRuntimeConfigDeclaration {
fn from(declaration: ConfigDeclaration) -> Self {
Self {
kind: declaration.kind.into(),
name: declaration.name,
reference: declaration.reference,
}
}
}
impl From<ConfigDeclarationKind> for server_api::WorkspaceRuntimeConfigDeclarationKind {
fn from(kind: ConfigDeclarationKind) -> Self {
match kind {
ConfigDeclarationKind::SecretRef => Self::SecretRef,
ConfigDeclarationKind::MountGrant => Self::MountGrant,
ConfigDeclarationKind::NetworkPolicy => Self::NetworkPolicy,
ConfigDeclarationKind::ShellPolicy => Self::ShellPolicy,
ConfigDeclarationKind::GitPolicy => Self::GitPolicy,
ConfigDeclarationKind::CapabilityGrant => Self::CapabilityGrant,
ConfigDeclarationKind::Unsupported => Self::Unsupported,
}
}
}
impl From<ProfileSourceArchive> for server_api::WorkspaceRuntimeProfileSourceArchive {
fn from(archive: ProfileSourceArchive) -> Self {
Self {
reference: archive.reference.into(),
content: archive.content,
}
}
}
impl From<ProfileSourceArchiveRef> for server_api::WorkspaceRuntimeProfileSourceArchiveRef {
fn from(reference: ProfileSourceArchiveRef) -> Self {
Self {
id: reference.id,
digest: reference.digest,
size_bytes: reference.size_bytes,
source_graph: reference.source_graph.into(),
}
}
}
impl From<ProfileSourceGraphSummary> for server_api::WorkspaceRuntimeProfileSourceGraph {
fn from(summary: ProfileSourceGraphSummary) -> Self {
Self {
source_count: summary.source_count,
total_source_bytes: summary.total_source_bytes,
entrypoints: summary.entrypoints,
import_count: summary.import_count,
}
}
}
impl From<BackendResourceHandle> for server_api::WorkspaceRuntimeResourceHandle {
fn from(handle: BackendResourceHandle) -> Self {
Self {
kind: handle.kind.into(),
workspace_id: handle.workspace_id,
scope_id: handle.scope_id,
runtime_id: handle.runtime_id,
worker_id: handle.worker_id,
resource_id: handle.resource_id,
digest: handle.digest,
operation: handle.operation.into(),
expires_at_unix_seconds: handle.expires_at_unix_seconds,
nonce: handle.nonce,
revision: handle.revision,
generation: handle.generation,
max_bytes: handle.max_bytes,
content_type: handle.content_type,
redaction: handle.redaction.into(),
audit_correlation_id: handle.audit_correlation_id,
profile_source_graph: handle.profile_source_graph.map(Into::into),
}
}
}
impl From<BackendResourceKind> for server_api::WorkspaceRuntimeResourceKind {
fn from(kind: BackendResourceKind) -> Self {
match kind {
BackendResourceKind::ProfileSourceArchive => Self::ProfileSourceArchive,
BackendResourceKind::RepositorySshAccess => Self::RepositorySshAccess,
}
}
}
impl From<BackendResourceOperation> for server_api::WorkspaceRuntimeResourceOperation {
fn from(operation: BackendResourceOperation) -> Self {
match operation {
BackendResourceOperation::FetchArchive => Self::FetchArchive,
BackendResourceOperation::FetchOnce => Self::FetchOnce,
}
}
}
impl From<ResourceRedactionPolicy> for server_api::WorkspaceRuntimeResourceRedactionPolicy {
fn from(policy: ResourceRedactionPolicy) -> Self {
match policy {
ResourceRedactionPolicy::RuntimeInternalOnly => Self::RuntimeInternalOnly,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigBundleMetadata {
pub id: String,
+1
View File
@@ -53,3 +53,4 @@ webauthn-rs = { workspace = true }
tempfile.workspace = true
tower = { workspace = true, features = ["util"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
webauthn-authenticator-rs = { version = "0.5.5", default-features = false, features = ["softpasskey"] }
+254 -11
View File
@@ -14855,15 +14855,7 @@ fn workspace_runtime_config_result(
},
);
}
let body = serde_json::to_value(bundle)
.map(server_api::WorkspaceRuntimeConfigResponse)
.map_err(|error| {
ApiError::from(invalid_contract_value(
"Workspace Runtime Config response",
error,
))
.into_repository_api_error()
})?;
let body = server_api::WorkspaceRuntimeConfigResponse::from(bundle);
Ok(
server_api::server_api_responses::WorkspaceRuntimeConfig::Status200 {
body,
@@ -31612,6 +31604,255 @@ mod tests {
assert_ne!(same_origin.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn generated_passkey_routes_complete_registration_login_and_logout_cookie_lifecycle() {
use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey};
let workspace = tempfile::tempdir().unwrap();
let mut api = test_api(workspace.path()).await;
let AuthConfig::Passkey {
rp_id,
origin,
public_base_url,
cookie_name,
} = &mut api.config.auth;
*rp_id = "workspace.test".to_string();
*origin = "https://workspace.test".to_string();
*public_base_url = origin.clone();
let origin = origin.clone();
let cookie_name = cookie_name.clone();
let app = generated_auth_contract_router(ServerApiContractService::Workspace(api));
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let authenticator_origin = url::Url::parse(&origin).unwrap();
let registration_options_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/registration/options")
.header(ORIGIN, &origin)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyRegistrationOptionsRequest {
handle: "generated-passkey-user".to_string(),
display_name: Some("Generated Passkey User".to_string()),
browser_origin: None,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(registration_options_response.status(), StatusCode::OK);
let registration_options: PasskeyRegistrationOptionsResponse = serde_json::from_slice(
&to_bytes(registration_options_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
let registration_credential = authenticator
.do_registration(
authenticator_origin.clone(),
registration_options.public_key,
)
.unwrap();
let registration_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/registration/complete")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyRegistrationCompleteRequest {
challenge_id: registration_options.challenge_id,
credential: registration_credential,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(registration_response.status(), StatusCode::OK);
let registration_cookie = registration_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap()
.to_string();
assert!(registration_cookie.starts_with(&format!("{cookie_name}=")));
assert!(registration_cookie.contains("; Secure"));
assert!(registration_cookie.contains("; HttpOnly; SameSite=Lax"));
let registration_body: AuthUserResponse = serde_json::from_slice(
&to_bytes(registration_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(registration_body.user.handle, "generated-passkey-user");
let registration_cookie_pair = registration_cookie.split(';').next().unwrap().to_string();
let registered_whoami = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, &registration_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(registered_whoami.status(), StatusCode::OK);
let registered_whoami: Value = serde_json::from_slice(
&to_bytes(registered_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(
registered_whoami["actor"]["handle"],
"generated-passkey-user"
);
let login_options_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/login/options")
.header(ORIGIN, &origin)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyLoginOptionsRequest {
handle: Some("generated-passkey-user".to_string()),
browser_origin: None,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(login_options_response.status(), StatusCode::OK);
let login_options: PasskeyLoginOptionsResponse = serde_json::from_slice(
&to_bytes(login_options_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
let login_credential = authenticator
.do_authentication(authenticator_origin, login_options.public_key)
.unwrap();
let login_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/login/complete")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyLoginCompleteRequest {
challenge_id: login_options.challenge_id,
credential: login_credential,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(login_response.status(), StatusCode::OK);
let login_cookie = login_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap()
.to_string();
assert!(login_cookie.starts_with(&format!("{cookie_name}=")));
assert!(login_cookie.contains("; Secure"));
assert!(login_cookie.contains("; HttpOnly; SameSite=Lax"));
let login_body: AuthUserResponse = serde_json::from_slice(
&to_bytes(login_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(login_body.user, registration_body.user);
let login_cookie_pair = login_cookie.split(';').next().unwrap().to_string();
let logged_in_whoami = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, &login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logged_in_whoami.status(), StatusCode::OK);
let logged_in_whoami: Value = serde_json::from_slice(
&to_bytes(logged_in_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(
logged_in_whoami["actor"]["handle"],
"generated-passkey-user"
);
let logout_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/logout")
.header(axum::http::header::COOKIE, &login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logout_response.status(), StatusCode::OK);
let logout_cookie = logout_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap();
assert!(logout_cookie.starts_with(&format!("{cookie_name}=")));
assert!(logout_cookie.contains("Max-Age=0"));
let logged_out_whoami = app
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logged_out_whoami.status(), StatusCode::OK);
let logged_out_whoami: Value = serde_json::from_slice(
&to_bytes(logged_out_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert!(logged_out_whoami["actor"].is_null());
}
#[tokio::test]
async fn browser_session_set_and_clear_cookies_follow_public_https_scheme() {
for (scheme, secure) in [("http", false), ("https", true)] {
@@ -37723,9 +37964,11 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let etag = response.headers().get(ETAG).unwrap().clone();
assert_eq!(response.headers().get(CACHE_CONTROL).unwrap(), "no-cache");
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let contract_bundle: server_api::WorkspaceRuntimeConfigResponse =
serde_json::from_slice(&body).unwrap();
let bundle: worker_runtime::config_bundle::ConfigBundle =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
serde_json::from_value(serde_json::to_value(contract_bundle).unwrap()).unwrap();
assert!(bundle.profile_source_archive.is_some());
assert_eq!(
etag.to_str().unwrap(),
+431 -2
View File
@@ -13128,6 +13128,106 @@
],
"type": "object"
},
"WorkspaceRuntimeConfigDeclaration": {
"additionalProperties": false,
"properties": {
"kind": {
"$ref": "#/components/schemas/WorkspaceRuntimeConfigDeclarationKind"
},
"name": {
"type": "string"
},
"reference": {
"type": "string"
}
},
"required": [
"kind",
"name",
"reference"
],
"type": "object"
},
"WorkspaceRuntimeConfigDeclarationKind": {
"enum": [
"secret_ref",
"mount_grant",
"network_policy",
"shell_policy",
"git_policy",
"capability_grant",
"unsupported"
],
"type": "string"
},
"WorkspaceRuntimeConfigMetadata": {
"additionalProperties": false,
"properties": {
"created_at": {
"type": "string"
},
"digest": {
"type": "string"
},
"id": {
"type": "string"
},
"provenance": {
"$ref": "#/components/schemas/WorkspaceRuntimeConfigProvenance"
},
"revision": {
"type": "string"
},
"workspace_id": {
"type": "string"
}
},
"required": [
"id",
"digest",
"revision",
"workspace_id",
"created_at",
"provenance"
],
"type": "object"
},
"WorkspaceRuntimeConfigProfile": {
"additionalProperties": false,
"properties": {
"label": {
"type": [
"string",
"null"
]
},
"selector": {
"$ref": "#/components/schemas/WorkspaceRuntimeProfileSelector"
}
},
"required": [
"selector"
],
"type": "object"
},
"WorkspaceRuntimeConfigProvenance": {
"additionalProperties": false,
"properties": {
"detail": {
"type": [
"string",
"null"
]
},
"source": {
"type": "string"
}
},
"required": [
"source"
],
"type": "object"
},
"WorkspaceRuntimeConfigQuery": {
"additionalProperties": false,
"properties": {
@@ -13141,7 +13241,59 @@
"type": "object"
},
"WorkspaceRuntimeConfigResponse": {
"description": "Transparent JSON body for the Runtime-owned config bundle wire format."
"additionalProperties": false,
"description": "Canonical Runtime config-bundle response projected by Workspace authority.\n\nThis is intentionally a concrete transport DTO rather than an opaque JSON value so generated\nclients and OpenAPI consumers receive the same complete wire contract as the Runtime.",
"properties": {
"declarations": {
"items": {
"$ref": "#/components/schemas/WorkspaceRuntimeConfigDeclaration"
},
"type": "array"
},
"metadata": {
"$ref": "#/components/schemas/WorkspaceRuntimeConfigMetadata"
},
"profile_source_archive": {
"anyOf": [
{
"$ref": "#/components/schemas/WorkspaceRuntimeProfileSourceArchive"
},
{
"type": "null"
}
]
},
"profile_source_archive_handle": {
"anyOf": [
{
"$ref": "#/components/schemas/WorkspaceRuntimeResourceHandle"
},
{
"type": "null"
}
]
},
"profiles": {
"items": {
"$ref": "#/components/schemas/WorkspaceRuntimeConfigProfile"
},
"type": "array"
},
"prompt_catalog": {
"anyOf": [
{
"$ref": "#/components/schemas/WorkspaceRuntimePromptCatalog"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata"
],
"type": "object"
},
"WorkspaceRuntimeDetail": {
"additionalProperties": false,
@@ -13213,6 +13365,164 @@
],
"type": "object"
},
"WorkspaceRuntimeProfileSelector": {
"oneOf": [
{
"properties": {
"kind": {
"const": "builtin",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "named",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
}
]
},
"WorkspaceRuntimeProfileSourceArchive": {
"additionalProperties": false,
"properties": {
"content": {
"items": {
"format": "uint8",
"maximum": 255,
"minimum": 0,
"type": "integer"
},
"type": "array"
},
"reference": {
"$ref": "#/components/schemas/WorkspaceRuntimeProfileSourceArchiveRef"
}
},
"required": [
"reference",
"content"
],
"type": "object"
},
"WorkspaceRuntimeProfileSourceArchiveRef": {
"additionalProperties": false,
"properties": {
"digest": {
"type": "string"
},
"id": {
"type": "string"
},
"size_bytes": {
"format": "uint64",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"source_graph": {
"$ref": "#/components/schemas/WorkspaceRuntimeProfileSourceGraph"
}
},
"required": [
"id",
"digest",
"size_bytes",
"source_graph"
],
"type": "object"
},
"WorkspaceRuntimeProfileSourceGraph": {
"additionalProperties": false,
"properties": {
"entrypoints": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"import_count": {
"format": "uint",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"source_count": {
"format": "uint",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"total_source_bytes": {
"format": "uint64",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
}
},
"required": [
"source_count",
"total_source_bytes",
"entrypoints",
"import_count"
],
"type": "object"
},
"WorkspaceRuntimePromptCatalog": {
"additionalProperties": false,
"properties": {
"catalog_digest": {
"type": "string"
},
"config_revision": {
"format": "uint64",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"schema_fingerprint": {
"type": "string"
},
"source_digest": {
"type": "string"
},
"templates": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"toolchain_fingerprint": {
"type": "string"
}
},
"required": [
"templates",
"config_revision",
"schema_fingerprint",
"toolchain_fingerprint",
"catalog_digest"
],
"type": "object"
},
"WorkspaceRuntimeResource": {
"properties": {
"arch": {
@@ -13270,6 +13580,125 @@
],
"type": "object"
},
"WorkspaceRuntimeResourceHandle": {
"additionalProperties": false,
"properties": {
"audit_correlation_id": {
"type": "string"
},
"content_type": {
"type": "string"
},
"digest": {
"type": "string"
},
"expires_at_unix_seconds": {
"format": "int64",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"generation": {
"format": "uint64",
"maximum": 9007199254740991,
"minimum": 0,
"type": [
"integer",
"null"
]
},
"kind": {
"$ref": "#/components/schemas/WorkspaceRuntimeResourceKind"
},
"max_bytes": {
"format": "uint64",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"nonce": {
"type": "string"
},
"operation": {
"$ref": "#/components/schemas/WorkspaceRuntimeResourceOperation"
},
"profile_source_graph": {
"anyOf": [
{
"$ref": "#/components/schemas/WorkspaceRuntimeProfileSourceGraph"
},
{
"type": "null"
}
]
},
"redaction": {
"$ref": "#/components/schemas/WorkspaceRuntimeResourceRedactionPolicy"
},
"resource_id": {
"type": "string"
},
"revision": {
"type": "string"
},
"runtime_id": {
"type": [
"string",
"null"
]
},
"scope_id": {
"type": [
"string",
"null"
]
},
"worker_id": {
"type": [
"string",
"null"
]
},
"workspace_id": {
"type": "string"
}
},
"required": [
"kind",
"workspace_id",
"resource_id",
"digest",
"operation",
"expires_at_unix_seconds",
"nonce",
"revision",
"max_bytes",
"content_type",
"redaction",
"audit_correlation_id"
],
"type": "object"
},
"WorkspaceRuntimeResourceKind": {
"enum": [
"profile_source_archive",
"repository_ssh_access"
],
"type": "string"
},
"WorkspaceRuntimeResourceOperation": {
"enum": [
"fetch_archive",
"fetch_once"
],
"type": "string"
},
"WorkspaceRuntimeResourceRedactionPolicy": {
"enum": [
"runtime_internal_only"
],
"type": "string"
},
"WorkspaceSigningIdentityPublic": {
"additionalProperties": false,
"description": "Public metadata for a Workspace signing identity. Private material and its\nstorage reference are deliberately not part of this wire authority.",
@@ -13464,7 +13893,7 @@
"info": {
"title": "Yoi Server API",
"version": "0.1.0",
"x-yoi-source-digest": "sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21"
"x-yoi-source-digest": "sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f"
},
"openapi": "3.1.0",
"paths": {
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// 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
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:c76b20e52333efb49a28f9db63d711561bfdc2871e98fd3cbf00d364f2992c21)
// Input contract: 0.1.0 (sha256:2463eb461342aceb84c87e7aa6bdcf73e28e3866349a555bd4a0d795db671a3f)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check