Skip to content

Commit 2debfdb

Browse files
committed
feat(auth): add OpenAPI AK/SK auth domain for token plan
- add openapi auth requirement with command-scoped access key flags and paired credential resolution - persist OpenAPI credentials through auth login/status/logout using access_key_* config fields - route token-plan commands through the centralized ACS signing client - keep legacy openapi_access_key_* config readable while rejecting it as a new config set key - refresh docs, generated references, telemetry authMethod, and e2e coverage
1 parent 49095c3 commit 2debfdb

41 files changed

Lines changed: 877 additions & 235 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,17 @@ Required for console capability commands (`app list`, `usage free`, `usage stats
165165
bl auth login --console
166166
```
167167

168-
### Alibaba Cloud AK/SK (Token Plan only)
168+
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
169169

170170
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
171171

172172
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
173173
174174
```bash
175+
# Option 1: Login command (persisted to ~/.bailian/config.json)
176+
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
177+
178+
# Option 2: Environment variables
175179
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
176180
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
177181
export BAILIAN_WORKSPACE_ID=ws-...

README.zh.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,17 @@ bl text chat --api-key sk-xxxxx --message "你好"
163163
bl auth login --console
164164
```
165165

166-
### 阿里云 AK/SK(仅 Token Plan)
166+
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
167167

168168
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
169169

170170
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
171171
172172
```bash
173+
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
174+
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
175+
176+
# 方式二:环境变量
173177
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
174178
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
175179
export BAILIAN_WORKSPACE_ID=ws-...

docs/agents/auth-change.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ config ──┘ │
1616
├─ buildSettings(sources) → ctx.settings
1717
1818
├─ resolveApiKey(sources) → model-domain Client
19-
└─ resolveConsole(sources) → console-domain Client
19+
├─ resolveConsole(sources) → console-domain Client
20+
└─ resolveOpenApi(sources) → OpenAPI Client
2021
2122
defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx)
2223
```
@@ -25,22 +26,26 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
2526

2627
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
2728
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent/workspace
29+
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
2830
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
2931

30-
### 双凭证并存(API Key + Console)
32+
### 多凭证并存
3133

32-
`~/.bailian/config.json` 可同时保存 `api_key``access_token`。登录任一种方式不得删除另一种:
34+
`~/.bailian/config.json` 可同时保存 `api_key``access_token``access_key_*`。登录任一种方式不得删除另一种:
3335

3436
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
3537
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
38+
- `bl auth login --open-api ...` 只更新 `access_key_id` / `access_key_secret`
3639
- `bl auth logout --console` 只清 `access_token`
37-
- `bl auth logout``api_key` + `access_token`
40+
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret`
41+
- `bl auth logout``api_key` + `access_token` + `access_key_*`
3842

3943
解析分工:
4044

4145
- `resolveApiKey()``auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
4246
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`
4347
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
48+
- `resolveOpenApi()``auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
4449
- `describeAuthState()``auth status` / banner / telemetry 使用的只读快照
4550

4651
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore()` / `ctx.configStore()` 的窄接口操作落盘。
@@ -84,12 +89,13 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
8489
- 新增/调整登录 flag 与流程
8590
- 持久化只走 `ctx.authStore().login(...)`
8691
- [ ] `packages/commands/src/commands/auth/status.ts`:
87-
- 分别显示 model / console 鉴权状态,并 mask token
92+
- 分别显示 model / console / openapi 鉴权状态,并 mask token
8893
- [ ] `packages/commands/src/commands/auth/logout.ts`:
8994
- 清理范围与双凭证并存规则一致
9095
- [ ] 新的业务命令设置正确 `auth`:
9196
- 模型域请求 → `auth: "apiKey"`
9297
- Console Gateway → `auth: "console"`
98+
- 阿里云 OpenAPI 请求 → `auth: "openapi"`
9399
- 本地/登录/配置 → `auth: "none"`
94100

95101
### D. 用户面文档
@@ -110,11 +116,14 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
110116
unset DASHSCOPE_API_KEY DASHSCOPE_ACCESS_TOKEN
111117
HOME=/tmp/empty node packages/cli/src/main.ts auth status
112118

113-
# flag 注入
114-
node packages/cli/src/main.ts auth status --api-key sk-xxx
119+
# flag 注入(凭证域 flag 只在对应业务命令可见,auth status 不接收)
120+
node packages/cli/src/main.ts text chat --message hi --api-key sk-xxx --dry-run
121+
node packages/cli/src/main.ts token-plan list-seats --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
122+
node packages/cli/src/main.ts auth login --open-api --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
115123

116124
# env 注入
117125
DASHSCOPE_API_KEY=sk-xxx node packages/cli/src/main.ts auth status
126+
ALIBABA_CLOUD_ACCESS_KEY_ID=ak-xxx ALIBABA_CLOUD_ACCESS_KEY_SECRET=sec-xxx node packages/cli/src/main.ts auth status
118127
```
119128

120129
Console 登录/网关相关改动:

docs/agents/url-change.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
core/config/schema.ts ← API endpoint / 文档站(region-aware)
1414
REGIONS{cn, us, intl} dashscope.aliyuncs.com 等
1515
DOCS_HOSTS{cn, us, intl} help.aliyun.com/zh/model-studio
16-
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (POP API)
16+
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (OpenAPI)
1717
1818
runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
1919
BAILIAN_CONSOLE_ROOT bailian.console.aliyun.com

packages/cli/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,17 @@ Required for console capability commands (`app list`, `usage free`, `usage stats
165165
bl auth login --console
166166
```
167167

168-
### Alibaba Cloud AK/SK (Token Plan only)
168+
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
169169

170170
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
171171

172172
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
173173
174174
```bash
175+
# Option 1: Login command (persisted to ~/.bailian/config.json)
176+
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
177+
178+
# Option 2: Environment variables
175179
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
176180
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
177181
export BAILIAN_WORKSPACE_ID=ws-...

packages/cli/README.zh.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,17 @@ bl text chat --api-key sk-xxxxx --message "你好"
163163
bl auth login --console
164164
```
165165

166-
### 阿里云 AK/SK(仅 Token Plan)
166+
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
167167

168168
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
169169

170170
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
171171
172172
```bash
173+
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
174+
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
175+
176+
# 方式二:环境变量
173177
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
174178
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
175179
export BAILIAN_WORKSPACE_ID=ws-...

packages/cli/tests/e2e/auth.e2e.test.ts

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { readFileSync } from "fs";
2+
import { join } from "path";
13
import { describe, expect, test } from "vite-plus/test";
2-
import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts";
4+
import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, runCli } from "./helpers.ts";
35

46
/**
57
* Auth 相关 E2E:只验证 CLI 进程能正常解析参数并退出。
@@ -18,6 +20,7 @@ describe("e2e: auth", () => {
1820
expect(exitCode, stderr).toBe(0);
1921
expect(stderr).toMatch(/login|api-key/i);
2022
expect(stderr).toMatch(/--console-site/);
23+
expect(stderr).toMatch(/--open-api/);
2124
});
2225

2326
test("auth logout --help 正常退出", async () => {
@@ -35,7 +38,58 @@ describe("e2e: auth", () => {
3538
test("auth login 缺少 --api-key 时报用法错误并退出 (2)", async () => {
3639
const { stderr, exitCode } = await runCli(["auth", "login", "--quiet"]);
3740
expect(exitCode, stderr).toBe(2);
38-
expect(stderr).toMatch(/--api-key|Usage:/i);
41+
expect(stderr).toMatch(/Choose exactly one login mode/);
42+
});
43+
44+
test("auth login 一次只能选择一种登录模式", async () => {
45+
const { stderr, exitCode } = await runCli([
46+
"auth",
47+
"login",
48+
"--console",
49+
"--api-key",
50+
"sk-e2e-placeholder",
51+
]);
52+
expect(exitCode).toBe(2);
53+
expect(stderr).toMatch(/Choose exactly one login mode/);
54+
});
55+
56+
test("auth login 模式专属参数不能脱离对应模式", async () => {
57+
const openApiFlagOnly = await runCli(["auth", "login", "--access-key-id", "LTAI-e2e"]);
58+
expect(openApiFlagOnly.exitCode).toBe(2);
59+
expect(openApiFlagOnly.stderr).toMatch(/Use --open-api with --access-key-id/);
60+
61+
const baseUrlWithoutApiKey = await runCli([
62+
"auth",
63+
"login",
64+
"--console",
65+
"--base-url",
66+
"https://dashscope.aliyuncs.com",
67+
]);
68+
expect(baseUrlWithoutApiKey.exitCode).toBe(2);
69+
expect(baseUrlWithoutApiKey.stderr).toMatch(/Use --base-url only with --api-key/);
70+
71+
const consoleSiteWithoutConsole = await runCli([
72+
"auth",
73+
"login",
74+
"--api-key",
75+
"sk-e2e-placeholder",
76+
"--console-site",
77+
"international",
78+
]);
79+
expect(consoleSiteWithoutConsole.exitCode).toBe(2);
80+
expect(consoleSiteWithoutConsole.stderr).toMatch(/Use --console-site only with --console/);
81+
});
82+
83+
test("auth login --open-api 要求 AK/SK 成对输入", async () => {
84+
const { stderr, exitCode } = await runCli([
85+
"auth",
86+
"login",
87+
"--open-api",
88+
"--access-key-id",
89+
"LTAI-e2e",
90+
]);
91+
expect(exitCode).toBe(2);
92+
expect(stderr).toMatch(/Provide --access-key-id and --access-key-secret with --open-api/);
3993
});
4094

4195
test("auth login --dry-run --api-key 不发起校验与落盘", async () => {
@@ -71,7 +125,7 @@ describe("e2e: auth", () => {
71125
expect(exitCode).toBe(2);
72126
const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } };
73127
expect(err.error?.code).toBe(2);
74-
expect(err.error?.message).toMatch(/--api-key|console/i);
128+
expect(err.error?.message).toMatch(/Choose exactly one login mode/);
75129
});
76130

77131
test("auth logout --dry-run 不写入配置", async () => {
@@ -138,4 +192,77 @@ describe("e2e: auth", () => {
138192
expect(exitCode).not.toBe(0);
139193
expect(stderr).toMatch(/Unknown flag.*--base-url/);
140194
});
195+
196+
test("auth status 展示 env OpenAPI AK/SK 且不接受 OpenAPI flag 覆盖", async () => {
197+
const { stdout, stderr, exitCode } = await runCli(["auth", "status", "--output", "json"], {
198+
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-e2e-placeholder",
199+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "secret-e2e-placeholder",
200+
});
201+
expect(exitCode, stderr).toBe(0);
202+
const data = parseStdoutJson<{
203+
authenticated?: boolean;
204+
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
205+
}>(stdout);
206+
expect(data.authenticated).toBe(true);
207+
expect(data.openapi?.source).toBe("env");
208+
expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-placeholder");
209+
expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-placeholder");
210+
211+
const denied = await runCli(["auth", "status", "--access-key-id", "ak"]);
212+
expect(denied.exitCode).not.toBe(0);
213+
expect(denied.stderr).toMatch(/Unknown flag.*--access-key-id/);
214+
});
215+
216+
test("auth login --open-api 持久化 OpenAPI AK/SK 并支持单独 logout", async () => {
217+
const configDir = makeE2eOutputDir("auth-openapi-login");
218+
const env = {
219+
BAILIAN_CONFIG_DIR: configDir,
220+
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
221+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
222+
};
223+
224+
const login = await runCli(
225+
[
226+
"auth",
227+
"login",
228+
"--open-api",
229+
"--access-key-id",
230+
"LTAI-e2e-login-placeholder",
231+
"--access-key-secret",
232+
"secret-e2e-login-placeholder",
233+
],
234+
env,
235+
);
236+
expect(login.exitCode, login.stderr).toBe(0);
237+
expect(login.stderr).toMatch(/OpenAPI credentials saved/);
238+
239+
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
240+
string,
241+
unknown
242+
>;
243+
expect(config.access_key_id).toBe("LTAI-e2e-login-placeholder");
244+
expect(config.access_key_secret).toBe("secret-e2e-login-placeholder");
245+
expect(config.openapi_access_key_id).toBeUndefined();
246+
expect(config.openapi_access_key_secret).toBeUndefined();
247+
248+
const status = await runCli(["auth", "status", "--output", "json"], env);
249+
expect(status.exitCode, status.stderr).toBe(0);
250+
const data = parseStdoutJson<{
251+
authenticated?: boolean;
252+
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
253+
}>(status.stdout);
254+
expect(data.authenticated).toBe(true);
255+
expect(data.openapi?.source).toBe("config");
256+
expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-login-placeholder");
257+
expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-login-placeholder");
258+
259+
const logout = await runCli(["auth", "logout", "--open-api"], env);
260+
expect(logout.exitCode, logout.stderr).toBe(0);
261+
expect(logout.stderr).toMatch(/Cleared access_key_id/);
262+
263+
const after = await runCli(["auth", "status", "--output", "json"], env);
264+
expect(after.exitCode, after.stderr).toBe(0);
265+
const afterData = parseStdoutJson<{ authenticated?: boolean; openapi?: unknown }>(after.stdout);
266+
expect(afterData.openapi).toBeUndefined();
267+
});
141268
});

packages/cli/tests/e2e/config.e2e.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,34 @@ describe("e2e: config", () => {
122122
const data = parseStdoutJson<{ would_set?: { default_text_model?: string } }>(stdout);
123123
expect(data.would_set?.default_text_model).toBe("qwen3.7-max");
124124
});
125+
126+
test("config set --dry-run 支持 AccessKey 短字段别名", async () => {
127+
const { stdout, stderr, exitCode } = await runCli([
128+
"config",
129+
"set",
130+
"--dry-run",
131+
"--key",
132+
"access-key-id",
133+
"--value",
134+
"LTAI-config-placeholder",
135+
"--output",
136+
"json",
137+
]);
138+
expect(exitCode, stderr).toBe(0);
139+
const data = parseStdoutJson<{ would_set?: { access_key_id?: string } }>(stdout);
140+
expect(data.would_set?.access_key_id).toBe("LTAI-config-placeholder");
141+
});
142+
143+
test("config set 不接受旧 OpenAPI AccessKey 字段名", async () => {
144+
const { stderr, exitCode } = await runCli([
145+
"config",
146+
"set",
147+
"--key",
148+
"openapi_access_key_id",
149+
"--value",
150+
"LTAI-config-placeholder",
151+
]);
152+
expect(exitCode).toBe(2);
153+
expect(stderr).toMatch(/Invalid config key|openapi_access_key_id/);
154+
});
125155
});

0 commit comments

Comments
 (0)