CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

feat(oauth-client): add StaticGating and wire interactive stage into run_pipeline

Adds StaticGating struct to pipeline.rs to track static check prerequisites
needed by the interactive stage. InteractiveOptions and InteractiveDriveMode are
moved from interactive.rs to pipeline.rs as they are needed by run_pipeline.
The run_pipeline function now conditionally runs the interactive stage if
opts.interactive is Some, building a StaticGating view from static check
results and passing it to interactive::run.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

+89 -5
+89 -5
src/commands/test/oauth/client/pipeline.rs
··· 264 264 // Pipeline runner and report types for Task 3. 265 265 266 266 use crate::common::identity::HttpClient; 267 - use crate::common::report::{LabelerReport, ReportHeader}; 267 + use crate::common::report::{CheckStatus, LabelerReport, ReportHeader}; 268 + 269 + /// Options for the interactive stage. 270 + pub struct InteractiveOptions { 271 + /// Port to bind the fake AS on (0 = ephemeral). 272 + pub bind_port: Option<u16>, 273 + /// Public base URL to advertise (defaults to 127.0.0.1:<port>). 274 + pub public_base_url: Option<Url>, 275 + /// Drive mode: wait for external client or drive an RP in-process. 276 + pub drive_mode: InteractiveDriveMode, 277 + } 278 + 279 + /// Chooses between waiting for an external client or driving an RP in-process. 280 + pub enum InteractiveDriveMode { 281 + /// Wait for tokio::signal::ctrl_c() before inspecting the request log. 282 + WaitForExternalClient, 283 + /// Drive an in-process RelyingParty via factory. 284 + DriveRpInProcess { 285 + /// Factory to build RP instances. 286 + rp_factory: std::sync::Arc<dyn crate::common::oauth::relying_party::RpFactory>, 287 + }, 288 + } 289 + 290 + /// Gate table: static check results that must pass before interactive checks can run. 291 + #[derive(Debug, Clone)] 292 + pub struct StaticGating { 293 + /// Status of the scope_present check. 294 + pub scope_present: CheckStatus, 295 + /// Status of the dpop_bound_required check. 296 + pub dpop_bound_required: CheckStatus, 297 + /// Status of the keys_have_alg check (for confidential clients). 298 + pub keys_have_alg: CheckStatus, 299 + /// Status of the grant_types check. 300 + pub grant_types_includes_authorization_code: CheckStatus, 301 + /// Status of the refresh_token grant type check. 302 + pub grant_types_includes_refresh_token: CheckStatus, 303 + } 268 304 269 305 /// Options passed to the pipeline runner. 270 306 pub struct OauthClientOptions<'a> { ··· 274 310 pub jwks: &'a dyn jwks::JwksFetcher, 275 311 /// Whether to emit verbose diagnostics. 276 312 pub verbose: bool, 313 + /// Optional interactive stage options. 314 + pub interactive: Option<&'a InteractiveOptions>, 277 315 } 278 316 279 317 /// Report wrapper for OAuth client conformance tests. ··· 317 355 target: OauthClientTarget, 318 356 opts: OauthClientOptions<'_>, 319 357 ) -> OauthClientReport { 358 + use crate::common::oauth::clock::RealClock; 359 + use std::sync::Arc; 360 + 320 361 // Build report header. 321 362 let target_str = match &target { 322 363 OauthClientTarget::HttpsUrl(url) => url.to_string(), ··· 360 401 } 361 402 362 403 // Run JWKS stage (consumes metadata facts). 363 - let jwks_output = if let Some(metadata_facts) = metadata_output.facts { 364 - jwks::run(&metadata_facts, opts.jwks).await 404 + let jwks_output = if let Some(metadata_facts) = &metadata_output.facts { 405 + jwks::run(metadata_facts, opts.jwks).await 365 406 } else { 366 407 jwks::emit_all_blocked_by("oauth_client::metadata::raw_document_deserializes") 367 408 }; 368 409 for result in jwks_output.results { 369 410 report.record(result); 370 411 } 371 - // TODO(Phase 7): thread JwksFacts into the interactive stage. Currently dropped. 372 - let _ = jwks_output.facts; 412 + 413 + // Run interactive stage if requested. 414 + if let Some(interactive_opts) = opts.interactive { 415 + // Build StaticGating view from the report so far. 416 + let mut static_gating = StaticGating { 417 + scope_present: CheckStatus::Pass, 418 + dpop_bound_required: CheckStatus::Pass, 419 + keys_have_alg: CheckStatus::Pass, 420 + grant_types_includes_authorization_code: CheckStatus::Pass, 421 + grant_types_includes_refresh_token: CheckStatus::Pass, 422 + }; 423 + 424 + // Scan the report for key static check results. 425 + for check in &report.0.results { 426 + match check.id { 427 + "oauth_client::metadata::scope_present" => { 428 + static_gating.scope_present = check.status; 429 + } 430 + "oauth_client::metadata::dpop_bound_required" => { 431 + static_gating.dpop_bound_required = check.status; 432 + } 433 + "oauth_client::jws::keys_have_alg" => { 434 + static_gating.keys_have_alg = check.status; 435 + } 436 + "oauth_client::metadata::grant_types_includes_authorization_code" => { 437 + static_gating.grant_types_includes_authorization_code = check.status; 438 + } 439 + _ => {} 440 + } 441 + } 442 + 443 + let clock = Arc::new(RealClock); 444 + let interactive_output = interactive::run( 445 + static_gating, 446 + metadata_output.facts.as_ref(), 447 + jwks_output.facts.as_ref(), 448 + interactive_opts, 449 + clock, 450 + ) 451 + .await; 452 + 453 + for result in interactive_output.results { 454 + report.record(result); 455 + } 456 + } 373 457 374 458 // Mark the report as finished. 375 459 report.finish();