Merge branch 'fawney19:main' into main

This commit is contained in:
ZheFox
2026-07-28 13:56:34 +08:00
committed by GitHub
117 changed files with 10467 additions and 1253 deletions
@@ -6,6 +6,7 @@ use aether_contracts::{
use aether_crypto::{
decrypt_python_fernet_ciphertext, encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY,
};
use aether_data::repository::background_tasks::InMemoryBackgroundTaskRepository;
use aether_data::repository::management_tokens::{
InMemoryManagementTokenRepository, ManagementTokenReadRepository,
};
@@ -15,6 +16,7 @@ use aether_data::repository::oauth_providers::{
use aether_data::repository::pool_scores::InMemoryPoolMemberScoreRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::proxy_nodes::InMemoryProxyNodeRepository;
use aether_data_contracts::repository::background_tasks::BackgroundTaskReadRepository;
use aether_data_contracts::repository::pool_scores::{
GetPoolMemberScoresByIdsQuery, PoolMemberHardState, PoolMemberIdentity, PoolScoreReadRepository,
};
@@ -26,7 +28,7 @@ use axum::response::{IntoResponse, Response};
use axum::routing::{any, delete, get, patch, post, put};
use axum::{extract::Request, Json, Router};
use http::{HeaderMap, HeaderValue, StatusCode};
use serde_json::json;
use serde_json::{json, Value};
use super::super::{
build_router_with_state, build_state_with_execution_runtime_override, hash_management_token,
@@ -309,17 +311,489 @@ async fn gateway_handles_admin_provider_oauth_supported_types_locally_with_trust
let items = payload.as_array().expect("items should be array");
assert_eq!(items.len(), 6);
assert_eq!(items[0]["provider_type"], "claude_code");
assert_eq!(
items[0]["authorize_url"],
"https://claude.ai/oauth/authorize"
);
assert_eq!(
items[0]["token_url"],
"https://platform.claude.com/v1/oauth/token"
);
assert_eq!(
items[0]["redirect_uri"],
"https://platform.claude.com/oauth/code/callback"
);
assert_eq!(items[0]["supports_cookie_authorization"], true);
assert_eq!(items[1]["provider_type"], "codex");
assert_eq!(items[2]["provider_type"], "chatgpt_web");
assert_eq!(items[3]["provider_type"], "gemini_cli");
assert_eq!(items[4]["provider_type"], "antigravity");
assert_eq!(items[5]["provider_type"], "windsurf");
assert!(items[1..]
.iter()
.all(|item| item["supports_cookie_authorization"] == false));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[test]
fn gateway_authorizes_claude_cookie_without_persisting_cookie() {
run_admin_oauth_test(
"gateway_authorizes_claude_cookie_without_persisting_cookie",
gateway_authorizes_claude_cookie_without_persisting_cookie_impl,
);
}
async fn gateway_authorizes_claude_cookie_without_persisting_cookie_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| {
let execution_plans_inner = Arc::clone(&execution_plans_clone);
async move {
execution_plans_inner
.lock()
.expect("mutex should lock")
.push(plan.clone());
let json_body = match plan.request_id.as_str() {
"provider-oauth:claude-cookie-organizations" => json!([
{"uuid": "org-personal", "raven_type": "personal"},
{"uuid": "org-team", "raven_type": "team"}
]),
"provider-oauth:claude-cookie-authorize" => {
let state = plan
.body
.json_body
.as_ref()
.and_then(|body| body.get("state"))
.and_then(serde_json::Value::as_str)
.expect("authorize plan should contain state");
let mut redirect =
url::Url::parse("https://platform.claude.com/oauth/code/callback")
.expect("redirect URL should parse");
redirect
.query_pairs_mut()
.append_pair("code", "claude-authorization-code")
.append_pair("state", state);
json!({"redirect_uri": redirect.to_string()})
}
"provider-oauth:exchange-code" => json!({
"access_token": "sk-ant-oat01-created",
"refresh_token": "sk-ant-ort01-created",
"expires_in": 3600,
"organization": {"uuid": "org-team"},
"account": {
"uuid": "account-claude-123",
"email_address": "[email protected]"
}
}),
unexpected => panic!("unexpected execution plan: {unexpected}"),
};
Json(json!({
"request_id": plan.request_id,
"status_code": 200,
"headers": {"content-type": "application/json"},
"body": {"json_body": json_body}
}))
}
}),
);
let mut provider = sample_provider("provider-claude", "claude", 10);
provider.provider_type = "claude_code".to_string();
provider.proxy = Some(json!({
"mode": "tunnel",
"node_id": "proxy-node-claude",
"url": "http://proxy.example:8080",
"enabled": true
}));
let endpoint = sample_endpoint(
"endpoint-claude-messages",
"provider-claude",
"claude:messages",
"https://api.anthropic.com",
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![endpoint],
vec![],
));
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let state = build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_repository_for_tests(
provider_catalog_repository.clone(),
)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
)
.with_provider_oauth_token_url_for_tests(
"claude_code_cookie_base_url",
"https://claude.example",
)
.with_provider_oauth_token_url_for_tests(
"claude_code",
"https://platform.example/v1/oauth/token",
);
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-claude/cookie-authorize",
Some(json!({
"cookie": "Cookie: other=value; sessionKey=sk-ant-sid01-secret; theme=dark",
"name": "claude-cookie-account"
})),
)
.await;
let status = response.status();
let payload: serde_json::Value = serde_json::from_slice(
&to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should read"),
)
.expect("response should be JSON");
assert_eq!(status, StatusCode::OK, "payload={payload}");
assert_eq!(payload["provider_type"], "claude_code");
assert_eq!(payload["has_refresh_token"], true);
assert_eq!(payload["temporary"], false);
assert_eq!(payload["email"], "[email protected]");
let keys = provider_catalog_repository
.list_keys_by_provider_ids(&["provider-claude".to_string()])
.await
.expect("keys should load");
assert_eq!(keys.len(), 1);
let decrypted_api_key = decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,
keys[0]
.encrypted_api_key
.as_deref()
.expect("api key should be encrypted"),
)
.expect("api key should decrypt");
assert_eq!(decrypted_api_key, "sk-ant-oat01-created");
let decrypted_auth_config = decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,
keys[0]
.encrypted_auth_config
.as_deref()
.expect("auth config should be encrypted"),
)
.expect("auth config should decrypt");
let auth_config: serde_json::Value =
serde_json::from_str(&decrypted_auth_config).expect("auth config should parse");
assert_eq!(auth_config["refresh_token"], "sk-ant-ort01-created");
assert_eq!(auth_config["org_uuid"], "org-team");
assert_eq!(auth_config["account_uuid"], "account-claude-123");
assert_eq!(auth_config["email"], "[email protected]");
assert!(!decrypted_auth_config.contains("sk-ant-sid01-secret"));
assert!(!decrypted_auth_config.contains("sessionKey"));
assert!(auth_config.get("cookie").is_none());
let plans = execution_plans.lock().expect("mutex should lock");
assert_eq!(plans.len(), 3);
for plan in &plans[..2] {
assert_eq!(
plan.headers.get("cookie").map(String::as_str),
Some("sessionKey=sk-ant-sid01-secret")
);
assert_eq!(
plan.headers
.get(EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER)
.map(String::as_str),
Some("false")
);
assert_eq!(
plan.proxy.as_ref().and_then(|proxy| proxy.mode.as_deref()),
Some("tunnel")
);
assert!(plan.transport_profile.is_none());
}
let token_plan = &plans[2];
assert_eq!(token_plan.request_id, "provider-oauth:exchange-code");
assert!(!token_plan.headers.contains_key("cookie"));
assert!(token_plan
.body
.json_body
.as_ref()
.is_some_and(|body| body.get("scope").is_none()));
execution_runtime_handle.abort();
}
#[test]
fn gateway_batch_authorizes_claude_cookies_as_redacted_task() {
run_admin_oauth_test(
"gateway_batch_authorizes_claude_cookies_as_redacted_task",
gateway_batch_authorizes_claude_cookies_as_redacted_task_impl,
);
}
async fn gateway_batch_authorizes_claude_cookies_as_redacted_task_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| {
let execution_plans_inner = Arc::clone(&execution_plans_clone);
async move {
execution_plans_inner
.lock()
.expect("mutex should lock")
.push(plan.clone());
let json_body = match plan.request_id.as_str() {
"provider-oauth:claude-cookie-organizations" => {
json!([{"uuid": "org-team", "raven_type": "team"}])
}
"provider-oauth:claude-cookie-authorize" => {
let cookie = plan
.headers
.get("cookie")
.map(String::as_str)
.expect("authorize plan should contain cookie");
let label = if cookie.contains("batch-sid-one") {
"one"
} else if cookie.contains("batch-sid-two") {
"two"
} else {
panic!("unexpected Cookie authorization input")
};
let state = plan
.body
.json_body
.as_ref()
.and_then(|body| body.get("state"))
.and_then(serde_json::Value::as_str)
.expect("authorize plan should contain state");
let mut redirect =
url::Url::parse("https://platform.claude.com/oauth/code/callback")
.expect("redirect URL should parse");
redirect
.query_pairs_mut()
.append_pair("code", format!("code-{label}").as_str())
.append_pair("state", state);
json!({"redirect_uri": redirect.to_string()})
}
"provider-oauth:exchange-code" => {
let code = plan
.body
.json_body
.as_ref()
.and_then(|body| body.get("code"))
.and_then(serde_json::Value::as_str)
.expect("token plan should contain code");
let label = code.strip_prefix("code-").expect("code should be tagged");
json!({
"access_token": format!("sk-ant-oat01-{label}"),
"refresh_token": format!("sk-ant-ort01-{label}"),
"expires_in": 3600,
"organization": {"uuid": "org-team"},
"account": {
"uuid": format!("account-{label}"),
"email_address": format!("{label}@example.com")
}
})
}
unexpected => panic!("unexpected execution plan: {unexpected}"),
};
Json(json!({
"request_id": plan.request_id,
"status_code": 200,
"headers": {"content-type": "application/json"},
"body": {"json_body": json_body}
}))
}
}),
);
let mut provider = sample_provider("provider-claude", "claude", 10);
provider.provider_type = "claude_code".to_string();
provider.proxy = Some(json!({
"mode": "tunnel",
"node_id": "proxy-node-claude",
"url": "http://proxy.example:8080",
"enabled": true
}));
let endpoint = sample_endpoint(
"endpoint-claude-messages",
"provider-claude",
"claude:messages",
"https://api.anthropic.com",
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![endpoint],
vec![],
));
let background_task_repository = Arc::new(InMemoryBackgroundTaskRepository::default());
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let state = build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_repository_for_tests(
provider_catalog_repository.clone(),
)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY)
.with_background_task_repository_for_tests(background_task_repository.clone()),
)
.with_provider_oauth_token_url_for_tests(
"claude_code_cookie_base_url",
"https://claude.example",
)
.with_provider_oauth_token_url_for_tests(
"claude_code",
"https://platform.example/v1/oauth/token",
);
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-claude/cookie-authorize/tasks",
Some(json!({
"cookies": [
"sessionKey=batch-sid-one",
"foo=bar",
"Cookie: sessionKey=batch-sid-one",
"sessionKey=batch-sid-two"
]
})),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
let submitted: serde_json::Value = serde_json::from_slice(
&to_bytes(response.into_body(), usize::MAX)
.await
.expect("submitted body should read"),
)
.expect("submitted body should parse");
assert_eq!(submitted["status"], "submitted");
assert_eq!(submitted["total"], 4);
assert_eq!(submitted["import_kind"], "cookie_authorize");
let task_id = submitted["task_id"]
.as_str()
.expect("task id should exist")
.to_string();
assert!(task_id.starts_with("claude-cookie-"));
let mut status_payload = Value::Null;
for _ in 0..80 {
let response = local_admin_provider_oauth_response(
&state,
http::Method::GET,
format!(
"/api/admin/provider-oauth/providers/provider-claude/cookie-authorize/tasks/{task_id}"
)
.as_str(),
None,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
status_payload = serde_json::from_slice(
&to_bytes(response.into_body(), usize::MAX)
.await
.expect("status body should read"),
)
.expect("status body should parse");
if status_payload["status"] == "completed" {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert_eq!(status_payload["status"], "completed");
assert_eq!(status_payload["import_kind"], "cookie_authorize");
assert_eq!(status_payload["total"], 4);
assert_eq!(status_payload["processed"], 4);
assert_eq!(status_payload["success"], 2);
assert_eq!(status_payload["failed"], 2);
assert_eq!(status_payload["created_count"], 2);
assert_eq!(status_payload["replaced_count"], 0);
let error_samples = status_payload["error_samples"]
.as_array()
.expect("error samples should be an array");
assert_eq!(error_samples.len(), 2);
assert_eq!(error_samples[0]["index"], 1);
assert_eq!(error_samples[1]["index"], 2);
let status_text = status_payload.to_string();
for forbidden in ["sessionKey", "batch-sid-one", "batch-sid-two"] {
assert!(!status_text.contains(forbidden), "forbidden={forbidden}");
}
let raw_task_state = state
.load_provider_oauth_batch_task_for_tests(
format!("provider_oauth_batch_task:{task_id}").as_str(),
)
.expect("raw task state should exist");
for forbidden in ["sessionKey", "batch-sid-one", "batch-sid-two"] {
assert!(
!raw_task_state.contains(forbidden),
"raw task state contains {forbidden}"
);
}
let background_run = background_task_repository
.find_run(&task_id)
.await
.expect("background run should load")
.expect("background run should exist");
let background_text = serde_json::to_string(&background_run)
.expect("background run should serialize for assertion");
for forbidden in ["sessionKey", "batch-sid-one", "batch-sid-two"] {
assert!(
!background_text.contains(forbidden),
"forbidden={forbidden}"
);
}
let keys = provider_catalog_repository
.list_keys_by_provider_ids(&["provider-claude".to_string()])
.await
.expect("keys should load");
assert_eq!(keys.len(), 2);
for key in &keys {
let auth_config = decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,
key.encrypted_auth_config
.as_deref()
.expect("auth config should be encrypted"),
)
.expect("auth config should decrypt");
assert!(!auth_config.contains("batch-sid"));
assert!(!auth_config.contains("sessionKey"));
}
let plans = execution_plans.lock().expect("mutex should lock");
assert_eq!(plans.len(), 6);
assert_eq!(
plans
.iter()
.filter(|plan| plan.request_id == "provider-oauth:claude-cookie-organizations")
.count(),
2
);
assert_eq!(
plans
.iter()
.filter(|plan| plan.request_id == "provider-oauth:claude-cookie-authorize")
.count(),
2
);
assert_eq!(
plans
.iter()
.filter(|plan| plan.request_id == "provider-oauth:exchange-code")
.count(),
2
);
assert!(plans.iter().all(|plan| {
plan.proxy.as_ref().and_then(|proxy| proxy.mode.as_deref()) == Some("tunnel")
}));
execution_runtime_handle.abort();
}
#[test]
fn gateway_handles_admin_provider_oauth_device_authorize_for_windsurf_browser() {
run_admin_oauth_test(
@@ -7955,6 +8429,7 @@ async fn gateway_handles_admin_provider_oauth_unavailable_routes_locally_with_tr
let client = reqwest::Client::new();
for path in [
"/api/admin/provider-oauth/providers/provider-123/import-refresh-token",
"/api/admin/provider-oauth/providers/provider-123/cookie-authorize/tasks",
"/api/admin/provider-oauth/providers/provider-123/batch-import",
"/api/admin/provider-oauth/providers/provider-123/batch-import/tasks",
"/api/admin/provider-oauth/providers/provider-123/device-authorize",
@@ -279,11 +279,24 @@ async fn gateway_handles_admin_pool_scheduling_presets_locally_with_trusted_admi
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let items = payload.as_array().expect("payload should be an array");
assert_eq!(items.len(), 14);
assert_eq!(items[0]["name"], "lru");
assert_eq!(items[1]["name"], "cache_affinity");
assert_eq!(items[8]["name"], "pro_first");
assert_eq!(items[13]["name"], "team_first");
assert_eq!(items.len(), 15);
let preset_names = items
.iter()
.filter_map(|item| item["name"].as_str())
.collect::<Vec<_>>();
assert_eq!(preset_names.first(), Some(&"lru"));
assert_eq!(preset_names.get(1), Some(&"cache_affinity"));
assert_eq!(preset_names.last(), Some(&"team_first"));
let preset_index = |name| {
preset_names
.iter()
.position(|preset_name| *preset_name == name)
.unwrap_or_else(|| panic!("missing scheduling preset {name}"))
};
assert!(preset_index("cost_first") < preset_index("free_team_first"));
assert!(preset_index("free_team_first") < preset_index("free_first"));
assert!(preset_index("pro_first") < preset_index("team_first"));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();