Merge commit 'refs/pull/563/head' of github-fawney19:fawney19/Aether into review/pr-562

This commit is contained in:
fawney19
2026-05-25 23:44:02 +08:00
3 changed files with 112 additions and 10 deletions
@@ -76,6 +76,26 @@ fn coerce_admin_provider_oauth_import_str(value: Option<&serde_json::Value>) ->
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
} }
fn json_import_expiry_value(value: Option<&serde_json::Value>) -> Option<u64> {
let value = value?;
json_u64_value(Some(value)).or_else(|| {
value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
.and_then(|value| u64::try_from(value.timestamp()).ok())
})
}
fn json_import_expiry_from_keys(
object: &serde_json::Map<String, serde_json::Value>,
keys: &[&str],
) -> Option<u64> {
keys.iter()
.find_map(|key| json_import_expiry_value(object.get(*key)))
}
fn grok_cookie_value(raw: &str, name: &str) -> Option<String> { fn grok_cookie_value(raw: &str, name: &str) -> Option<String> {
raw.trim() raw.trim()
.strip_prefix("Cookie:") .strip_prefix("Cookie:")
@@ -188,6 +208,8 @@ fn extract_admin_provider_oauth_batch_import_entry(
object object
.get("sso_token") .get("sso_token")
.or_else(|| object.get("ssoToken")) .or_else(|| object.get("ssoToken"))
.or_else(|| object.get("session_token"))
.or_else(|| object.get("sessionToken"))
.or(grok_token_alias), .or(grok_token_alias),
) )
.or_else(|| { .or_else(|| {
@@ -238,7 +260,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
refresh_token refresh_token
}; };
let expires_at = let expires_at =
json_u64_value(object.get("expires_at").or_else(|| object.get("expiresAt"))); json_import_expiry_from_keys(object, &["expires_at", "expiresAt", "expired"]);
let account_id = coerce_admin_provider_oauth_import_str( let account_id = coerce_admin_provider_oauth_import_str(
object object
.get("account_id") .get("account_id")
@@ -619,6 +641,21 @@ mod tests {
assert_eq!(entries[0].email.as_deref(), Some("[email protected]")); assert_eq!(entries[0].email.as_deref(), Some("[email protected]"));
} }
#[test]
fn parses_common_chatgpt_web_json_aliases() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"chatgpt_web",
r#"[{"session_token":"session-1","expired":"2030-01-01T00:00:00Z","chatgpt_account_id":"acc-1","chatgpt_plan_type":"plus"}]"#,
);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].refresh_token, None);
assert_eq!(entries[0].access_token.as_deref(), Some("session-1"));
assert_eq!(entries[0].expires_at, Some(1_893_456_000));
assert_eq!(entries[0].account_id.as_deref(), Some("acc-1"));
assert_eq!(entries[0].plan_type.as_deref(), Some("plus"));
}
#[test] #[test]
fn parses_plain_jwt_line_as_access_token() { fn parses_plain_jwt_line_as_access_token() {
let token = unsigned_jwt(json!({ let token = unsigned_jwt(json!({
@@ -106,12 +106,21 @@ fn import_payload_string_any(
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
} }
fn import_payload_u64( fn import_payload_u64_any(
payload: &serde_json::Map<String, serde_json::Value>, payload: &serde_json::Map<String, serde_json::Value>,
snake_case: &str, keys: &[&str],
camel_case: &str,
) -> Option<u64> { ) -> Option<u64> {
json_u64_value(payload.get(snake_case).or_else(|| payload.get(camel_case))) keys.iter().find_map(|key| {
let value = payload.get(*key)?;
json_u64_value(Some(value)).or_else(|| {
value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
.and_then(|value| u64::try_from(value.timestamp()).ok())
})
})
} }
fn apply_single_import_hints( fn apply_single_import_hints(
@@ -409,9 +418,17 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
let refresh_token_input = import_payload_string(&raw_payload, "refresh_token", "refreshToken"); let refresh_token_input = import_payload_string(&raw_payload, "refresh_token", "refreshToken");
let access_token_input = import_payload_string_any( let access_token_input = import_payload_string_any(
&raw_payload, &raw_payload,
&["access_token", "accessToken", "sso_token", "ssoToken"], &[
"access_token",
"accessToken",
"sso_token",
"ssoToken",
"session_token",
"sessionToken",
],
); );
let imported_expires_at = import_payload_u64(&raw_payload, "expires_at", "expiresAt"); let imported_expires_at =
import_payload_u64_any(&raw_payload, &["expires_at", "expiresAt", "expired"]);
let name = raw_payload let name = raw_payload
.get("name") .get("name")
.and_then(serde_json::Value::as_str) .and_then(serde_json::Value::as_str)
@@ -622,8 +639,39 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::sanitize_windsurf_import_error; use super::{import_payload_string_any, import_payload_u64_any, sanitize_windsurf_import_error};
use aether_oauth::core::OAuthError; use aether_oauth::core::OAuthError;
use serde_json::json;
#[test]
fn single_import_accepts_session_token_alias() {
let payload = json!({
"session_token": "session-1",
})
.as_object()
.cloned()
.expect("payload should be an object");
assert_eq!(
import_payload_string_any(&payload, &["access_token", "session_token"]).as_deref(),
Some("session-1")
);
}
#[test]
fn single_import_accepts_iso_expired_alias() {
let payload = json!({
"expired": "2030-01-01T00:00:00Z",
})
.as_object()
.cloned()
.expect("payload should be an object");
assert_eq!(
import_payload_u64_any(&payload, &["expires_at", "expiresAt", "expired"]),
Some(1_893_456_000)
);
}
#[test] #[test]
fn windsurf_import_error_redacts_http_body() { fn windsurf_import_error_redacts_http_body() {
@@ -1408,6 +1408,8 @@ function parseImportText(text: string): {
const refreshTokenCamel = obj.refreshToken const refreshTokenCamel = obj.refreshToken
const accessToken = obj.access_token const accessToken = obj.access_token
const accessTokenCamel = obj.accessToken const accessTokenCamel = obj.accessToken
const sessionToken = obj.session_token
const sessionTokenCamel = obj.sessionToken
const grokSsoToken = isGrokProvider.value const grokSsoToken = isGrokProvider.value
? normalizeStringField(obj.sso_token) ?? normalizeStringField(obj.ssoToken) ?? normalizeStringField(obj.token) ?? grokCookieImport?.access_token ? normalizeStringField(obj.sso_token) ?? normalizeStringField(obj.ssoToken) ?? normalizeStringField(obj.token) ?? grokCookieImport?.access_token
: undefined : undefined
@@ -1417,12 +1419,15 @@ function parseImportText(text: string): {
const normalizedAccessToken = typeof accessToken === 'string' && accessToken.trim() const normalizedAccessToken = typeof accessToken === 'string' && accessToken.trim()
? accessToken.trim() ? accessToken.trim()
: (typeof accessTokenCamel === 'string' && accessTokenCamel.trim() ? accessTokenCamel.trim() : undefined) : (typeof accessTokenCamel === 'string' && accessTokenCamel.trim() ? accessTokenCamel.trim() : undefined)
const importedAccessToken = normalizedAccessToken ?? grokSsoToken const normalizedSessionToken = typeof sessionToken === 'string' && sessionToken.trim()
? sessionToken.trim()
: (typeof sessionTokenCamel === 'string' && sessionTokenCamel.trim() ? sessionTokenCamel.trim() : undefined)
const importedAccessToken = normalizedAccessToken ?? grokSsoToken ?? normalizedSessionToken
if (normalizedRefreshToken || importedAccessToken) { if (normalizedRefreshToken || importedAccessToken) {
return { return {
refresh_token: normalizedRefreshToken, refresh_token: normalizedRefreshToken,
access_token: importedAccessToken, access_token: importedAccessToken,
expires_at: normalizeNumberField(obj.expires_at) ?? normalizeNumberField(obj.expiresAt), expires_at: normalizeExpiryField(obj.expires_at) ?? normalizeExpiryField(obj.expiresAt) ?? normalizeExpiryField(obj.expired),
name: (typeof obj.name === 'string' ? obj.name : undefined) || (typeof obj.oauth_email === 'string' ? obj.oauth_email : undefined), name: (typeof obj.name === 'string' ? obj.name : undefined) || (typeof obj.oauth_email === 'string' ? obj.oauth_email : undefined),
email: normalizeStringField(obj.email) ?? normalizeStringField(obj.oauth_email), email: normalizeStringField(obj.email) ?? normalizeStringField(obj.oauth_email),
account_id: normalizeStringField(obj.account_id) ?? normalizeStringField(obj.accountId) ?? normalizeStringField(obj.chatgpt_account_id) ?? normalizeStringField(obj.chatgptAccountId), account_id: normalizeStringField(obj.account_id) ?? normalizeStringField(obj.accountId) ?? normalizeStringField(obj.chatgpt_account_id) ?? normalizeStringField(obj.chatgptAccountId),
@@ -1534,6 +1539,18 @@ function normalizeNumberField(value: unknown): number | undefined {
return undefined return undefined
} }
function normalizeExpiryField(value: unknown): number | undefined {
const numeric = normalizeNumberField(value)
if (numeric) return numeric
if (typeof value === 'string' && value.trim()) {
const parsed = Date.parse(value.trim())
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed / 1000)
}
}
return undefined
}
function isLikelyJwtToken(token: string): boolean { function isLikelyJwtToken(token: string): boolean {
const parts = token.trim().split('.') const parts = token.trim().split('.')
if (parts.length !== 3 || parts.some(part => !part)) return false if (parts.length !== 3 || parts.some(part => !part)) return false