feat: 支持凭据级 region/machineId 配置及自动认证方式检测 (#24)
Browse files* feat: 支持凭据级 region/machineId 配置及自动认证方式检测
凭据级配置:
- 新增 credentials.region 字段,用于 OIDC token 刷新时指定 endpoint 区域
- 新增 credentials.machineId 字段,支持凭据级机器码配置
- 优先级: 凭据级 > config.json 全局配置 > refreshToken 派生
machineId 格式兼容:
- 支持 64 字符十六进制格式(原有)
- 支持 UUID 格式(如 2582956e-cc88-4669-b546-07adbffcb894),自动转换为 64 字符
认证方式自动检测:
- 当 authMethod 未指定时,根据 clientId/clientSecret 存在与否自动判断
- 有 clientId + clientSecret → idc 认证
- 否则 → social 认证
文件变更:
- src/kiro/machine_id.rs: 添加 normalize_machine_id 函数
- src/kiro/token_manager.rs: 添加 region 优先级逻辑和 authMethod 自动检测
- src/kiro/model/credentials.rs: 新增 region/machineId 字段及测试
- src/admin/types.rs, src/admin/service.rs: Admin API 支持新字段
- README.md, credentials.example.*.json: 文档和示例更新
* feat: 支持凭据级 region/machineId 配置及自动认证方式检测
凭据级配置:
- 新增 credentials.region 字段,用于 OIDC token 刷新时指定 endpoint 区域
- 新增 credentials.machineId 字段,支持凭据级机器码配置
- 优先级: 凭据级 > config.json 全局配置 > refreshToken 派生
machineId 格式兼容:
- 支持 64 字符十六进制格式(原有)
- 支持 UUID 格式(如 2582956e-cc88-4669-b546-07adbffcb894),自动转换为 64 字符
认证方式自动检测:
- 当 authMethod 未指定时,根据 clientId/clientSecret 存在与否自动判断
- 有 clientId + clientSecret → idc 认证
- 否则 → social 认证
文件变更:
- src/kiro/machine_id.rs: 添加 normalize_machine_id 函数
- src/kiro/token_manager.rs: 添加 region 优先级逻辑和 authMethod 自动检测
- src/kiro/model/credentials.rs: 新增 region/machineId 字段及测试
- src/admin/types.rs, src/admin/service.rs: Admin API 支持新字段
- README.md, credentials.example.*.json: 文档和示例更新
- README.md +5 -1
- credentials.example.idc.json +4 -2
- credentials.example.multiple.json +3 -0
- credentials.example.social.json +3 -2
- src/admin/service.rs +2 -0
- src/admin/types.rs +8 -0
- src/kiro/machine_id.rs +90 -5
- src/kiro/model/credentials.rs +194 -0
- src/kiro/token_manager.rs +136 -3
|
@@ -101,6 +101,7 @@ cargo build --release
|
|
| 101 |
"authMethod": "idc",
|
| 102 |
"clientId": "xxxxxxxxx",
|
| 103 |
"clientSecret": "xxxxxxxxx",
|
|
|
|
| 104 |
"priority": 1
|
| 105 |
}
|
| 106 |
]
|
|
@@ -111,6 +112,7 @@ cargo build --release
|
|
| 111 |
> - 单凭据最多重试 3 次,单请求最多重试 9 次
|
| 112 |
> - 自动故障转移到下一个可用凭据
|
| 113 |
> - 多凭据格式下 Token 刷新后自动回写到源文件
|
|
|
|
| 114 |
|
| 115 |
最小启动配置(social):
|
| 116 |
```json
|
|
@@ -194,6 +196,8 @@ curl http://127.0.0.1:8990/v1/messages \
|
|
| 194 |
| `clientId` | string | IdC 登录的客户端 ID(可选) |
|
| 195 |
| `clientSecret` | string | IdC 登录的客户端密钥(可选) |
|
| 196 |
| `priority` | number | 凭据优先级,数字越小越优先,默认为 0(多凭据格式时有效)|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
## 模型映射
|
| 199 |
|
|
@@ -344,4 +348,4 @@ MIT
|
|
| 344 |
- [kiro2api](https://github.com/caidaoli/kiro2api)
|
| 345 |
- [proxycast](https://github.com/aiclientproxy/proxycast)
|
| 346 |
|
| 347 |
-
本项目部分逻辑参考了以上的项目, 再次由衷的感谢!
|
|
|
|
| 101 |
"authMethod": "idc",
|
| 102 |
"clientId": "xxxxxxxxx",
|
| 103 |
"clientSecret": "xxxxxxxxx",
|
| 104 |
+
"region": "us-east-2",
|
| 105 |
"priority": 1
|
| 106 |
}
|
| 107 |
]
|
|
|
|
| 112 |
> - 单凭据最多重试 3 次,单请求最多重试 9 次
|
| 113 |
> - 自动故障转移到下一个可用凭据
|
| 114 |
> - 多凭据格式下 Token 刷新后自动回写到源文件
|
| 115 |
+
> - 可选的 `region` 字段:用于 OIDC token 刷新时指定 endpoint 区域,未配置时回退到 config.json 的 region
|
| 116 |
|
| 117 |
最小启动配置(social):
|
| 118 |
```json
|
|
|
|
| 196 |
| `clientId` | string | IdC 登录的客户端 ID(可选) |
|
| 197 |
| `clientSecret` | string | IdC 登录的客户端密钥(可选) |
|
| 198 |
| `priority` | number | 凭据优先级,数字越小越优先,默认为 0(多凭据格式时有效)|
|
| 199 |
+
| `region` | string | 凭据级 region(可选),用于 OIDC token 刷新时指定 endpoint 的区域。未配置时回退到 config.json 的 region。注意:API 调用始终使用 config.json 的 region |
|
| 200 |
+
| `machineId` | string | 凭据级机器码(可选,64位十六进制)。未配置时回退到 config.json 的 machineId;都未配置时由 refreshToken 派生 |
|
| 201 |
|
| 202 |
## 模型映射
|
| 203 |
|
|
|
|
| 348 |
- [kiro2api](https://github.com/caidaoli/kiro2api)
|
| 349 |
- [proxycast](https://github.com/aiclientproxy/proxycast)
|
| 350 |
|
| 351 |
+
本项目部分逻辑参考了以上的项目, 再次由衷的感谢!
|
|
@@ -3,5 +3,7 @@
|
|
| 3 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 4 |
"authMethod": "idc",
|
| 5 |
"clientId": "xxxxxxxxx",
|
| 6 |
-
"clientSecret": "xxxxxxxxx"
|
| 7 |
-
|
|
|
|
|
|
|
|
|
| 3 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 4 |
"authMethod": "idc",
|
| 5 |
"clientId": "xxxxxxxxx",
|
| 6 |
+
"clientSecret": "xxxxxxxxx",
|
| 7 |
+
"region": "us-east-2",
|
| 8 |
+
"machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
| 9 |
+
}
|
|
@@ -3,6 +3,7 @@
|
|
| 3 |
"refreshToken": "xxxxxxxxxxxxxxxxxxxx",
|
| 4 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 5 |
"authMethod": "social",
|
|
|
|
| 6 |
"priority": 0
|
| 7 |
},
|
| 8 |
{
|
|
@@ -11,6 +12,8 @@
|
|
| 11 |
"authMethod": "idc",
|
| 12 |
"clientId": "xxxxxxxxx",
|
| 13 |
"clientSecret": "xxxxxxxxx",
|
|
|
|
|
|
|
| 14 |
"priority": 1
|
| 15 |
}
|
| 16 |
]
|
|
|
|
| 3 |
"refreshToken": "xxxxxxxxxxxxxxxxxxxx",
|
| 4 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 5 |
"authMethod": "social",
|
| 6 |
+
"machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
| 7 |
"priority": 0
|
| 8 |
},
|
| 9 |
{
|
|
|
|
| 12 |
"authMethod": "idc",
|
| 13 |
"clientId": "xxxxxxxxx",
|
| 14 |
"clientSecret": "xxxxxxxxx",
|
| 15 |
+
"region": "us-east-2",
|
| 16 |
+
"machineId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
| 17 |
"priority": 1
|
| 18 |
}
|
| 19 |
]
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"refreshToken": "xxxxxxxxxxxxxxxxxxxx",
|
| 3 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 4 |
-
"authMethod": "social"
|
| 5 |
-
|
|
|
|
|
|
| 1 |
{
|
| 2 |
"refreshToken": "xxxxxxxxxxxxxxxxxxxx",
|
| 3 |
"expiresAt": "2025-12-31T02:32:45.144Z",
|
| 4 |
+
"authMethod": "social",
|
| 5 |
+
"machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
| 6 |
+
}
|
|
@@ -128,6 +128,8 @@ impl AdminService {
|
|
| 128 |
client_id: req.client_id,
|
| 129 |
client_secret: req.client_secret,
|
| 130 |
priority: req.priority,
|
|
|
|
|
|
|
| 131 |
};
|
| 132 |
|
| 133 |
// 调用 token_manager 添加凭据
|
|
|
|
| 128 |
client_id: req.client_id,
|
| 129 |
client_secret: req.client_secret,
|
| 130 |
priority: req.priority,
|
| 131 |
+
region: req.region,
|
| 132 |
+
machine_id: req.machine_id,
|
| 133 |
};
|
| 134 |
|
| 135 |
// 调用 token_manager 添加凭据
|
|
@@ -78,6 +78,14 @@ pub struct AddCredentialRequest {
|
|
| 78 |
/// 优先级(可选,默认 0)
|
| 79 |
#[serde(default)]
|
| 80 |
pub priority: u32,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
fn default_auth_method() -> String {
|
|
|
|
| 78 |
/// 优先级(可选,默认 0)
|
| 79 |
#[serde(default)]
|
| 80 |
pub priority: u32,
|
| 81 |
+
|
| 82 |
+
/// 凭据级 Region 配置(用于 OIDC token 刷新)
|
| 83 |
+
/// 未配置时回退到 config.json 的全局 region
|
| 84 |
+
pub region: Option<String>,
|
| 85 |
+
|
| 86 |
+
/// 凭据级 Machine ID(可选,64 位字符串)
|
| 87 |
+
/// 未配置时回退到 config.json 的 machineId
|
| 88 |
+
pub machine_id: Option<String>,
|
| 89 |
}
|
| 90 |
|
| 91 |
fn default_auth_method() -> String {
|
|
@@ -6,14 +6,47 @@ use sha2::{Digest, Sha256};
|
|
| 6 |
use crate::kiro::model::credentials::KiroCredentials;
|
| 7 |
use crate::model::config::Config;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
/// 根据凭证信息生成唯一的 Machine ID
|
| 10 |
///
|
| 11 |
-
/// 优先使用
|
| 12 |
pub fn generate_from_credentials(credentials: &KiroCredentials, config: &Config) -> Option<String> {
|
| 13 |
-
// 如果配置了
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
if let Some(ref machine_id) = config.machine_id {
|
| 15 |
-
if
|
| 16 |
-
return Some(
|
| 17 |
}
|
| 18 |
}
|
| 19 |
|
|
@@ -60,10 +93,22 @@ mod tests {
|
|
| 60 |
assert_eq!(result, Some("a".repeat(64)));
|
| 61 |
}
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
#[test]
|
| 64 |
fn test_generate_with_refresh_token() {
|
| 65 |
let mut credentials = KiroCredentials::default();
|
| 66 |
-
credentials.refresh_token = Some("test_refresh_token".to_string());
|
| 67 |
let config = Config::default();
|
| 68 |
|
| 69 |
let result = generate_from_credentials(&credentials, &config);
|
|
@@ -79,4 +124,44 @@ mod tests {
|
|
| 79 |
let result = generate_from_credentials(&credentials, &config);
|
| 80 |
assert!(result.is_none());
|
| 81 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
|
|
|
| 6 |
use crate::kiro::model::credentials::KiroCredentials;
|
| 7 |
use crate::model::config::Config;
|
| 8 |
|
| 9 |
+
/// 标准化 machineId 格式
|
| 10 |
+
///
|
| 11 |
+
/// 支持以下格式:
|
| 12 |
+
/// - 64 字符十六进制字符串(直接返回)
|
| 13 |
+
/// - UUID 格式(如 "2582956e-cc88-4669-b546-07adbffcb894",移除连字符后补齐到 64 字符)
|
| 14 |
+
fn normalize_machine_id(machine_id: &str) -> Option<String> {
|
| 15 |
+
let trimmed = machine_id.trim();
|
| 16 |
+
|
| 17 |
+
// 如果已经是 64 字符,直接返回
|
| 18 |
+
if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
|
| 19 |
+
return Some(trimmed.to_string());
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// 尝试解析 UUID 格式(移除连字符)
|
| 23 |
+
let without_dashes: String = trimmed.chars().filter(|c| *c != '-').collect();
|
| 24 |
+
|
| 25 |
+
// UUID 去掉连字符后是 32 字符
|
| 26 |
+
if without_dashes.len() == 32 && without_dashes.chars().all(|c| c.is_ascii_hexdigit()) {
|
| 27 |
+
// 补齐到 64 字符(重复一次)
|
| 28 |
+
return Some(format!("{}{}", without_dashes, without_dashes));
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// 无法识别的格式
|
| 32 |
+
None
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
/// 根据凭证信息生成唯一的 Machine ID
|
| 36 |
///
|
| 37 |
+
/// 优先使用凭据级 machineId,其次使用 config.machineId,然后使用 refreshToken 生成
|
| 38 |
pub fn generate_from_credentials(credentials: &KiroCredentials, config: &Config) -> Option<String> {
|
| 39 |
+
// 如果配置了凭据级 machineId,优先使用
|
| 40 |
+
if let Some(ref machine_id) = credentials.machine_id {
|
| 41 |
+
if let Some(normalized) = normalize_machine_id(machine_id) {
|
| 42 |
+
return Some(normalized);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 如果配置了全局 machineId,作为默认值
|
| 47 |
if let Some(ref machine_id) = config.machine_id {
|
| 48 |
+
if let Some(normalized) = normalize_machine_id(machine_id) {
|
| 49 |
+
return Some(normalized);
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
|
|
|
| 93 |
assert_eq!(result, Some("a".repeat(64)));
|
| 94 |
}
|
| 95 |
|
| 96 |
+
#[test]
|
| 97 |
+
fn test_generate_with_credential_machine_id_overrides_config() {
|
| 98 |
+
let mut credentials = KiroCredentials::default();
|
| 99 |
+
credentials.machine_id = Some("b".repeat(64));
|
| 100 |
+
|
| 101 |
+
let mut config = Config::default();
|
| 102 |
+
config.machine_id = Some("a".repeat(64));
|
| 103 |
+
|
| 104 |
+
let result = generate_from_credentials(&credentials, &config);
|
| 105 |
+
assert_eq!(result, Some("b".repeat(64)));
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
#[test]
|
| 109 |
fn test_generate_with_refresh_token() {
|
| 110 |
let mut credentials = KiroCredentials::default();
|
| 111 |
+
credentials.refresh_token = Some("test_refresh_token".to_string());
|
| 112 |
let config = Config::default();
|
| 113 |
|
| 114 |
let result = generate_from_credentials(&credentials, &config);
|
|
|
|
| 124 |
let result = generate_from_credentials(&credentials, &config);
|
| 125 |
assert!(result.is_none());
|
| 126 |
}
|
| 127 |
+
|
| 128 |
+
#[test]
|
| 129 |
+
fn test_normalize_uuid_format() {
|
| 130 |
+
// UUID 格式应该被转换为 64 字符
|
| 131 |
+
let uuid = "2582956e-cc88-4669-b546-07adbffcb894";
|
| 132 |
+
let result = normalize_machine_id(uuid);
|
| 133 |
+
assert!(result.is_some());
|
| 134 |
+
let normalized = result.unwrap();
|
| 135 |
+
assert_eq!(normalized.len(), 64);
|
| 136 |
+
// UUID 去掉连字符后重复一次
|
| 137 |
+
assert_eq!(normalized, "2582956ecc884669b54607adbffcb8942582956ecc884669b54607adbffcb894");
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
#[test]
|
| 141 |
+
fn test_normalize_64_char_hex() {
|
| 142 |
+
// 64 字符十六进制应该直接返回
|
| 143 |
+
let hex64 = "a".repeat(64);
|
| 144 |
+
let result = normalize_machine_id(&hex64);
|
| 145 |
+
assert_eq!(result, Some(hex64));
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#[test]
|
| 149 |
+
fn test_normalize_invalid_format() {
|
| 150 |
+
// 无效格式应该返回 None
|
| 151 |
+
assert!(normalize_machine_id("invalid").is_none());
|
| 152 |
+
assert!(normalize_machine_id("too-short").is_none());
|
| 153 |
+
assert!(normalize_machine_id(&"g".repeat(64)).is_none()); // 非十六进制
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#[test]
|
| 157 |
+
fn test_generate_with_uuid_machine_id() {
|
| 158 |
+
let mut credentials = KiroCredentials::default();
|
| 159 |
+
credentials.machine_id = Some("2582956e-cc88-4669-b546-07adbffcb894".to_string());
|
| 160 |
+
|
| 161 |
+
let config = Config::default();
|
| 162 |
+
|
| 163 |
+
let result = generate_from_credentials(&credentials, &config);
|
| 164 |
+
assert!(result.is_some());
|
| 165 |
+
assert_eq!(result.as_ref().unwrap().len(), 64);
|
| 166 |
+
}
|
| 167 |
}
|
|
@@ -47,6 +47,16 @@ pub struct KiroCredentials {
|
|
| 47 |
#[serde(default)]
|
| 48 |
#[serde(skip_serializing_if = "is_zero")]
|
| 49 |
pub priority: u32,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
/// 判断是否为零(用于跳过序列化)
|
|
@@ -199,6 +209,8 @@ mod tests {
|
|
| 199 |
client_id: None,
|
| 200 |
client_secret: None,
|
| 201 |
priority: 0,
|
|
|
|
|
|
|
| 202 |
};
|
| 203 |
|
| 204 |
let json = creds.to_pretty_json().unwrap();
|
|
@@ -265,4 +277,186 @@ mod tests {
|
|
| 265 |
assert_eq!(list[1].refresh_token, Some("t3".to_string())); // priority 1
|
| 266 |
assert_eq!(list[2].refresh_token, Some("t1".to_string())); // priority 2
|
| 267 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
}
|
|
|
|
| 47 |
#[serde(default)]
|
| 48 |
#[serde(skip_serializing_if = "is_zero")]
|
| 49 |
pub priority: u32,
|
| 50 |
+
|
| 51 |
+
/// 凭据级 Region 配置(用于 OIDC token 刷新)
|
| 52 |
+
/// 未配置时回退到 config.json 的全局 region
|
| 53 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 54 |
+
pub region: Option<String>,
|
| 55 |
+
|
| 56 |
+
/// 凭据级 Machine ID 配置(可选)
|
| 57 |
+
/// 未配置时回退到 config.json 的 machineId;都未配置时由 refreshToken 派生
|
| 58 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 59 |
+
pub machine_id: Option<String>,
|
| 60 |
}
|
| 61 |
|
| 62 |
/// 判断是否为零(用于跳过序列化)
|
|
|
|
| 209 |
client_id: None,
|
| 210 |
client_secret: None,
|
| 211 |
priority: 0,
|
| 212 |
+
region: None,
|
| 213 |
+
machine_id: None,
|
| 214 |
};
|
| 215 |
|
| 216 |
let json = creds.to_pretty_json().unwrap();
|
|
|
|
| 277 |
assert_eq!(list[1].refresh_token, Some("t3".to_string())); // priority 1
|
| 278 |
assert_eq!(list[2].refresh_token, Some("t1".to_string())); // priority 2
|
| 279 |
}
|
| 280 |
+
|
| 281 |
+
// ============ Region 字段测试 ============
|
| 282 |
+
|
| 283 |
+
#[test]
|
| 284 |
+
fn test_region_field_parsing() {
|
| 285 |
+
// 测试解析包含 region 字段的 JSON
|
| 286 |
+
let json = r#"{
|
| 287 |
+
"refreshToken": "test_refresh",
|
| 288 |
+
"region": "us-east-1"
|
| 289 |
+
}"#;
|
| 290 |
+
|
| 291 |
+
let creds = KiroCredentials::from_json(json).unwrap();
|
| 292 |
+
assert_eq!(creds.refresh_token, Some("test_refresh".to_string()));
|
| 293 |
+
assert_eq!(creds.region, Some("us-east-1".to_string()));
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
#[test]
|
| 297 |
+
fn test_region_field_missing_backward_compat() {
|
| 298 |
+
// 测试向后兼容:不包含 region 字段的旧格式 JSON
|
| 299 |
+
let json = r#"{
|
| 300 |
+
"refreshToken": "test_refresh",
|
| 301 |
+
"authMethod": "social"
|
| 302 |
+
}"#;
|
| 303 |
+
|
| 304 |
+
let creds = KiroCredentials::from_json(json).unwrap();
|
| 305 |
+
assert_eq!(creds.refresh_token, Some("test_refresh".to_string()));
|
| 306 |
+
assert_eq!(creds.region, None);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
#[test]
|
| 310 |
+
fn test_region_field_serialization() {
|
| 311 |
+
// 测试序列化时正确输出 region 字段
|
| 312 |
+
let creds = KiroCredentials {
|
| 313 |
+
id: None,
|
| 314 |
+
access_token: None,
|
| 315 |
+
refresh_token: Some("test".to_string()),
|
| 316 |
+
profile_arn: None,
|
| 317 |
+
expires_at: None,
|
| 318 |
+
auth_method: None,
|
| 319 |
+
client_id: None,
|
| 320 |
+
client_secret: None,
|
| 321 |
+
priority: 0,
|
| 322 |
+
region: Some("eu-west-1".to_string()),
|
| 323 |
+
machine_id: None,
|
| 324 |
+
};
|
| 325 |
+
|
| 326 |
+
let json = creds.to_pretty_json().unwrap();
|
| 327 |
+
assert!(json.contains("region"));
|
| 328 |
+
assert!(json.contains("eu-west-1"));
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
#[test]
|
| 332 |
+
fn test_region_field_none_not_serialized() {
|
| 333 |
+
// 测试 region 为 None 时不序列化
|
| 334 |
+
let creds = KiroCredentials {
|
| 335 |
+
id: None,
|
| 336 |
+
access_token: None,
|
| 337 |
+
refresh_token: Some("test".to_string()),
|
| 338 |
+
profile_arn: None,
|
| 339 |
+
expires_at: None,
|
| 340 |
+
auth_method: None,
|
| 341 |
+
client_id: None,
|
| 342 |
+
client_secret: None,
|
| 343 |
+
priority: 0,
|
| 344 |
+
region: None,
|
| 345 |
+
machine_id: None,
|
| 346 |
+
};
|
| 347 |
+
|
| 348 |
+
let json = creds.to_pretty_json().unwrap();
|
| 349 |
+
assert!(!json.contains("region"));
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// ============ MachineId 字段测试 ============
|
| 353 |
+
|
| 354 |
+
#[test]
|
| 355 |
+
fn test_machine_id_field_parsing() {
|
| 356 |
+
let machine_id = "a".repeat(64);
|
| 357 |
+
let json = format!(
|
| 358 |
+
r#"{{
|
| 359 |
+
"refreshToken": "test_refresh",
|
| 360 |
+
"machineId": "{machine_id}"
|
| 361 |
+
}}"#
|
| 362 |
+
);
|
| 363 |
+
|
| 364 |
+
let creds = KiroCredentials::from_json(&json).unwrap();
|
| 365 |
+
assert_eq!(creds.refresh_token, Some("test_refresh".to_string()));
|
| 366 |
+
assert_eq!(creds.machine_id, Some(machine_id));
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
#[test]
|
| 370 |
+
fn test_machine_id_field_serialization() {
|
| 371 |
+
let mut creds = KiroCredentials::default();
|
| 372 |
+
creds.refresh_token = Some("test".to_string());
|
| 373 |
+
creds.machine_id = Some("b".repeat(64));
|
| 374 |
+
|
| 375 |
+
let json = creds.to_pretty_json().unwrap();
|
| 376 |
+
assert!(json.contains("machineId"));
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
#[test]
|
| 380 |
+
fn test_machine_id_field_none_not_serialized() {
|
| 381 |
+
let mut creds = KiroCredentials::default();
|
| 382 |
+
creds.refresh_token = Some("test".to_string());
|
| 383 |
+
creds.machine_id = None;
|
| 384 |
+
|
| 385 |
+
let json = creds.to_pretty_json().unwrap();
|
| 386 |
+
assert!(!json.contains("machineId"));
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
#[test]
|
| 390 |
+
fn test_multiple_credentials_with_different_regions() {
|
| 391 |
+
// 测试多凭据场景下不同凭据使用各自的 region
|
| 392 |
+
let json = r#"[
|
| 393 |
+
{"refreshToken": "t1", "region": "us-east-1"},
|
| 394 |
+
{"refreshToken": "t2", "region": "eu-west-1"},
|
| 395 |
+
{"refreshToken": "t3"}
|
| 396 |
+
]"#;
|
| 397 |
+
|
| 398 |
+
let config: CredentialsConfig = serde_json::from_str(json).unwrap();
|
| 399 |
+
let list = config.into_sorted_credentials();
|
| 400 |
+
|
| 401 |
+
assert_eq!(list[0].region, Some("us-east-1".to_string()));
|
| 402 |
+
assert_eq!(list[1].region, Some("eu-west-1".to_string()));
|
| 403 |
+
assert_eq!(list[2].region, None);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
#[test]
|
| 407 |
+
fn test_region_field_with_all_fields() {
|
| 408 |
+
// 测试包含所有字段的完整 JSON
|
| 409 |
+
let json = r#"{
|
| 410 |
+
"id": 1,
|
| 411 |
+
"accessToken": "access",
|
| 412 |
+
"refreshToken": "refresh",
|
| 413 |
+
"profileArn": "arn:aws:test",
|
| 414 |
+
"expiresAt": "2025-12-31T00:00:00Z",
|
| 415 |
+
"authMethod": "idc",
|
| 416 |
+
"clientId": "client123",
|
| 417 |
+
"clientSecret": "secret456",
|
| 418 |
+
"priority": 5,
|
| 419 |
+
"region": "ap-northeast-1"
|
| 420 |
+
}"#;
|
| 421 |
+
|
| 422 |
+
let creds = KiroCredentials::from_json(json).unwrap();
|
| 423 |
+
assert_eq!(creds.id, Some(1));
|
| 424 |
+
assert_eq!(creds.access_token, Some("access".to_string()));
|
| 425 |
+
assert_eq!(creds.refresh_token, Some("refresh".to_string()));
|
| 426 |
+
assert_eq!(creds.profile_arn, Some("arn:aws:test".to_string()));
|
| 427 |
+
assert_eq!(creds.expires_at, Some("2025-12-31T00:00:00Z".to_string()));
|
| 428 |
+
assert_eq!(creds.auth_method, Some("idc".to_string()));
|
| 429 |
+
assert_eq!(creds.client_id, Some("client123".to_string()));
|
| 430 |
+
assert_eq!(creds.client_secret, Some("secret456".to_string()));
|
| 431 |
+
assert_eq!(creds.priority, 5);
|
| 432 |
+
assert_eq!(creds.region, Some("ap-northeast-1".to_string()));
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
#[test]
|
| 436 |
+
fn test_region_roundtrip() {
|
| 437 |
+
// 测试序列化和反序列化的往返一致性
|
| 438 |
+
let original = KiroCredentials {
|
| 439 |
+
id: Some(42),
|
| 440 |
+
access_token: Some("token".to_string()),
|
| 441 |
+
refresh_token: Some("refresh".to_string()),
|
| 442 |
+
profile_arn: None,
|
| 443 |
+
expires_at: None,
|
| 444 |
+
auth_method: Some("social".to_string()),
|
| 445 |
+
client_id: None,
|
| 446 |
+
client_secret: None,
|
| 447 |
+
priority: 3,
|
| 448 |
+
region: Some("us-west-2".to_string()),
|
| 449 |
+
machine_id: Some("c".repeat(64)),
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
let json = original.to_pretty_json().unwrap();
|
| 453 |
+
let parsed = KiroCredentials::from_json(&json).unwrap();
|
| 454 |
+
|
| 455 |
+
assert_eq!(parsed.id, original.id);
|
| 456 |
+
assert_eq!(parsed.access_token, original.access_token);
|
| 457 |
+
assert_eq!(parsed.refresh_token, original.refresh_token);
|
| 458 |
+
assert_eq!(parsed.priority, original.priority);
|
| 459 |
+
assert_eq!(parsed.region, original.region);
|
| 460 |
+
assert_eq!(parsed.machine_id, original.machine_id);
|
| 461 |
+
}
|
| 462 |
}
|
|
@@ -132,7 +132,14 @@ pub(crate) async fn refresh_token(
|
|
| 132 |
validate_refresh_token(credentials)?;
|
| 133 |
|
| 134 |
// 根据 auth_method 选择刷新方式
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
match auth_method.to_lowercase().as_str() {
|
| 138 |
"idc" | "builder-id" => refresh_idc_token(credentials, config, proxy).await,
|
|
@@ -149,7 +156,8 @@ async fn refresh_social_token(
|
|
| 149 |
tracing::info!("正在刷新 Social Token...");
|
| 150 |
|
| 151 |
let refresh_token = credentials.refresh_token.as_ref().unwrap();
|
| 152 |
-
|
|
|
|
| 153 |
|
| 154 |
let refresh_url = format!("https://prod.{}.auth.desktop.kiro.dev/refreshToken", region);
|
| 155 |
let refresh_domain = format!("prod.{}.auth.desktop.kiro.dev", region);
|
|
@@ -232,7 +240,8 @@ async fn refresh_idc_token(
|
|
| 232 |
.as_ref()
|
| 233 |
.ok_or_else(|| anyhow::anyhow!("IdC 刷新需要 clientSecret"))?;
|
| 234 |
|
| 235 |
-
|
|
|
|
| 236 |
let refresh_url = format!("https://oidc.{}.amazonaws.com/token", region);
|
| 237 |
|
| 238 |
let client = build_client(proxy, 60)?;
|
|
@@ -1501,4 +1510,128 @@ mod tests {
|
|
| 1501 |
);
|
| 1502 |
assert_eq!(manager.available_count(), 0);
|
| 1503 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1504 |
}
|
|
|
|
| 132 |
validate_refresh_token(credentials)?;
|
| 133 |
|
| 134 |
// 根据 auth_method 选择刷新方式
|
| 135 |
+
// 如果未指定 auth_method,根据是否有 clientId/clientSecret 自动判断
|
| 136 |
+
let auth_method = credentials.auth_method.as_deref().unwrap_or_else(|| {
|
| 137 |
+
if credentials.client_id.is_some() && credentials.client_secret.is_some() {
|
| 138 |
+
"idc"
|
| 139 |
+
} else {
|
| 140 |
+
"social"
|
| 141 |
+
}
|
| 142 |
+
});
|
| 143 |
|
| 144 |
match auth_method.to_lowercase().as_str() {
|
| 145 |
"idc" | "builder-id" => refresh_idc_token(credentials, config, proxy).await,
|
|
|
|
| 156 |
tracing::info!("正在刷新 Social Token...");
|
| 157 |
|
| 158 |
let refresh_token = credentials.refresh_token.as_ref().unwrap();
|
| 159 |
+
// 优先使用凭据级 region,未配置时回退到 config.region
|
| 160 |
+
let region = credentials.region.as_ref().unwrap_or(&config.region);
|
| 161 |
|
| 162 |
let refresh_url = format!("https://prod.{}.auth.desktop.kiro.dev/refreshToken", region);
|
| 163 |
let refresh_domain = format!("prod.{}.auth.desktop.kiro.dev", region);
|
|
|
|
| 240 |
.as_ref()
|
| 241 |
.ok_or_else(|| anyhow::anyhow!("IdC 刷新需要 clientSecret"))?;
|
| 242 |
|
| 243 |
+
// 优先使用凭据级 region,未配置时回退到 config.region
|
| 244 |
+
let region = credentials.region.as_ref().unwrap_or(&config.region);
|
| 245 |
let refresh_url = format!("https://oidc.{}.amazonaws.com/token", region);
|
| 246 |
|
| 247 |
let client = build_client(proxy, 60)?;
|
|
|
|
| 1510 |
);
|
| 1511 |
assert_eq!(manager.available_count(), 0);
|
| 1512 |
}
|
| 1513 |
+
|
| 1514 |
+
// ============ 凭据级 Region 优先级测试 ============
|
| 1515 |
+
|
| 1516 |
+
/// 辅助函数:获取 OIDC 刷新使用的 region(用于测试)
|
| 1517 |
+
fn get_oidc_region_for_credential<'a>(
|
| 1518 |
+
credentials: &'a KiroCredentials,
|
| 1519 |
+
config: &'a Config,
|
| 1520 |
+
) -> &'a str {
|
| 1521 |
+
credentials.region.as_ref().unwrap_or(&config.region)
|
| 1522 |
+
}
|
| 1523 |
+
|
| 1524 |
+
#[test]
|
| 1525 |
+
fn test_credential_region_priority_uses_credential_region() {
|
| 1526 |
+
// 凭据配置了 region 时,应使用凭据的 region
|
| 1527 |
+
let mut config = Config::default();
|
| 1528 |
+
config.region = "us-west-2".to_string();
|
| 1529 |
+
|
| 1530 |
+
let mut credentials = KiroCredentials::default();
|
| 1531 |
+
credentials.region = Some("eu-west-1".to_string());
|
| 1532 |
+
|
| 1533 |
+
let region = get_oidc_region_for_credential(&credentials, &config);
|
| 1534 |
+
assert_eq!(region, "eu-west-1");
|
| 1535 |
+
}
|
| 1536 |
+
|
| 1537 |
+
#[test]
|
| 1538 |
+
fn test_credential_region_priority_fallback_to_config() {
|
| 1539 |
+
// 凭据未配置 region 时,应回退到 config.region
|
| 1540 |
+
let mut config = Config::default();
|
| 1541 |
+
config.region = "us-west-2".to_string();
|
| 1542 |
+
|
| 1543 |
+
let credentials = KiroCredentials::default();
|
| 1544 |
+
assert!(credentials.region.is_none());
|
| 1545 |
+
|
| 1546 |
+
let region = get_oidc_region_for_credential(&credentials, &config);
|
| 1547 |
+
assert_eq!(region, "us-west-2");
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
#[test]
|
| 1551 |
+
fn test_multiple_credentials_use_respective_regions() {
|
| 1552 |
+
// 多凭据场景下,不同凭据使用各自的 region
|
| 1553 |
+
let mut config = Config::default();
|
| 1554 |
+
config.region = "ap-northeast-1".to_string();
|
| 1555 |
+
|
| 1556 |
+
let mut cred1 = KiroCredentials::default();
|
| 1557 |
+
cred1.region = Some("us-east-1".to_string());
|
| 1558 |
+
|
| 1559 |
+
let mut cred2 = KiroCredentials::default();
|
| 1560 |
+
cred2.region = Some("eu-west-1".to_string());
|
| 1561 |
+
|
| 1562 |
+
let cred3 = KiroCredentials::default(); // 无 region,使用 config
|
| 1563 |
+
|
| 1564 |
+
assert_eq!(get_oidc_region_for_credential(&cred1, &config), "us-east-1");
|
| 1565 |
+
assert_eq!(get_oidc_region_for_credential(&cred2, &config), "eu-west-1");
|
| 1566 |
+
assert_eq!(
|
| 1567 |
+
get_oidc_region_for_credential(&cred3, &config),
|
| 1568 |
+
"ap-northeast-1"
|
| 1569 |
+
);
|
| 1570 |
+
}
|
| 1571 |
+
|
| 1572 |
+
#[test]
|
| 1573 |
+
fn test_idc_oidc_endpoint_uses_credential_region() {
|
| 1574 |
+
// 验证 IdC OIDC endpoint URL 使用凭据 region
|
| 1575 |
+
let mut config = Config::default();
|
| 1576 |
+
config.region = "us-west-2".to_string();
|
| 1577 |
+
|
| 1578 |
+
let mut credentials = KiroCredentials::default();
|
| 1579 |
+
credentials.region = Some("eu-central-1".to_string());
|
| 1580 |
+
|
| 1581 |
+
let region = get_oidc_region_for_credential(&credentials, &config);
|
| 1582 |
+
let refresh_url = format!("https://oidc.{}.amazonaws.com/token", region);
|
| 1583 |
+
|
| 1584 |
+
assert_eq!(refresh_url, "https://oidc.eu-central-1.amazonaws.com/token");
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
#[test]
|
| 1588 |
+
fn test_social_refresh_endpoint_uses_credential_region() {
|
| 1589 |
+
// 验证 Social refresh endpoint URL 使用凭据 region
|
| 1590 |
+
let mut config = Config::default();
|
| 1591 |
+
config.region = "us-west-2".to_string();
|
| 1592 |
+
|
| 1593 |
+
let mut credentials = KiroCredentials::default();
|
| 1594 |
+
credentials.region = Some("ap-southeast-1".to_string());
|
| 1595 |
+
|
| 1596 |
+
let region = get_oidc_region_for_credential(&credentials, &config);
|
| 1597 |
+
let refresh_url = format!("https://prod.{}.auth.desktop.kiro.dev/refreshToken", region);
|
| 1598 |
+
|
| 1599 |
+
assert_eq!(
|
| 1600 |
+
refresh_url,
|
| 1601 |
+
"https://prod.ap-southeast-1.auth.desktop.kiro.dev/refreshToken"
|
| 1602 |
+
);
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
#[test]
|
| 1606 |
+
fn test_api_call_still_uses_config_region() {
|
| 1607 |
+
// 验证 API 调用(如 getUsageLimits)仍使用 config.region
|
| 1608 |
+
// 这确保只有 OIDC 刷新使用凭据 region,API 调用行为不变
|
| 1609 |
+
let mut config = Config::default();
|
| 1610 |
+
config.region = "us-west-2".to_string();
|
| 1611 |
+
|
| 1612 |
+
let mut credentials = KiroCredentials::default();
|
| 1613 |
+
credentials.region = Some("eu-west-1".to_string());
|
| 1614 |
+
|
| 1615 |
+
// API 调用应使用 config.region,而非 credentials.region
|
| 1616 |
+
let api_region = &config.region;
|
| 1617 |
+
let api_host = format!("q.{}.amazonaws.com", api_region);
|
| 1618 |
+
|
| 1619 |
+
assert_eq!(api_host, "q.us-west-2.amazonaws.com");
|
| 1620 |
+
// 确认凭据 region 不影响 API 调用
|
| 1621 |
+
assert_ne!(api_region, credentials.region.as_ref().unwrap());
|
| 1622 |
+
}
|
| 1623 |
+
|
| 1624 |
+
#[test]
|
| 1625 |
+
fn test_credential_region_empty_string_treated_as_set() {
|
| 1626 |
+
// 空字符串 region 被视为已设置(虽然不推荐,但行为应一致)
|
| 1627 |
+
let mut config = Config::default();
|
| 1628 |
+
config.region = "us-west-2".to_string();
|
| 1629 |
+
|
| 1630 |
+
let mut credentials = KiroCredentials::default();
|
| 1631 |
+
credentials.region = Some("".to_string());
|
| 1632 |
+
|
| 1633 |
+
let region = get_oidc_region_for_credential(&credentials, &config);
|
| 1634 |
+
// 空字符串被视为已设置,不会回退到 config
|
| 1635 |
+
assert_eq!(region, "");
|
| 1636 |
+
}
|
| 1637 |
}
|