Skip to content

Commit 3fb0c72

Browse files
committed
feat(auth): add support for optional security token in CLI access token generation
1 parent 049eecd commit 3fb0c72

14 files changed

Lines changed: 125 additions & 51 deletions

File tree

packages/commands/src/commands/auth/generate-access-token.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ const FLAGS = {
1919
description: "Alibaba Cloud Access Key Secret",
2020
required: true,
2121
},
22+
securityToken: {
23+
type: "string",
24+
valueHint: "<token>",
25+
description: "Alibaba Cloud STS Security Token to store (optional)",
26+
},
2227
} satisfies FlagsDef;
2328

2429
export default defineCommand({
2530
description: "Generate a CLI access token using OpenAPI AK/SK",
2631
auth: "none",
27-
usageArgs: "--access-key-id <id> --access-key-secret <secret>",
32+
usageArgs: "--access-key-id <id> --access-key-secret <secret> --security-token <token>",
2833
flags: FLAGS,
29-
exampleArgs: ["--access-key-id LTAIxxxxx --access-key-secret xxxxx"],
34+
exampleArgs: ["--access-key-id LTAIxxxxx --access-key-secret xxxxx --security-token <token>"],
3035
async run(ctx) {
3136
const { identity, settings, flags } = ctx;
3237
const format = detectOutputFormat(settings.output);
@@ -37,6 +42,7 @@ export default defineCommand({
3742
baseUrl: ctx.client.baseUrl,
3843
accessKeyId: flags.accessKeyId,
3944
accessKeySecret: flags.accessKeySecret,
45+
securityToken: flags.securityToken || undefined,
4046
});
4147

4248
emitResult(resp, format);

packages/commands/src/commands/bootstrap/index.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
44
const API = {
55
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
66
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
7-
queryBuyResult: "zeldaEasy.broadscope-bailian.bill.queryBuyPostpaidResult",
8-
commodityOrderInfo: "zeldaEasy.broadscope-bailian.bill.postpaidCommodityOrderInfo",
9-
buyCommodity: "zeldaEasy.broadscope-bailian.bill.buyPostpaidCommodity",
7+
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
8+
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
9+
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
1010
} as const;
1111

1212
const POLL_INTERVAL_MS = 1000;
@@ -16,10 +16,13 @@ function sleep(ms: number): Promise<void> {
1616
return new Promise((resolve) => setTimeout(resolve, ms));
1717
}
1818

19+
function extractData(resp: any): any {
20+
return resp?.data?.DataV2?.data?.data;
21+
}
22+
1923
interface CommodityItem {
20-
commodity?: string;
24+
commodityCode?: string;
2125
status?: number;
22-
startDate?: string;
2326
}
2427

2528
export default defineCommand({
@@ -36,9 +39,21 @@ export default defineCommand({
3639
emitResult(
3740
{
3841
apis: [
39-
{ step: 1, api: API.loginInfo, description: "Check login & workspace status" },
40-
{ step: 2, api: API.initSpace, description: "Initialize workspace (if needed)" },
41-
{ step: 3, api: API.queryBuyResult, description: "Query postpaid order status" },
42+
{
43+
step: 1,
44+
api: API.loginInfo,
45+
description: "Check login & workspace status",
46+
},
47+
{
48+
step: 2,
49+
api: API.initSpace,
50+
description: "Initialize workspace (if needed)",
51+
},
52+
{
53+
step: 3,
54+
api: API.queryBuyResult,
55+
description: "Query postpaid order status",
56+
},
4257
{
4358
step: 4,
4459
api: API.commodityOrderInfo,
@@ -59,15 +74,21 @@ export default defineCommand({
5974
const verbose = settings.verbose;
6075
const callApi = async (api: string) => {
6176
if (verbose) process.stderr.write(`> ${api}\n`);
62-
const resp = await ctx.client.console<any>(api, {});
63-
if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`);
64-
return resp;
77+
try {
78+
const resp = await ctx.client.console<any>(api, {});
79+
if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`);
80+
return resp;
81+
} catch (err) {
82+
if (verbose) process.stderr.write(`< ERROR: ${err instanceof Error ? err.message : err}\n`);
83+
throw err;
84+
}
6585
};
6686

6787
// Step 1: Check login info
6888
emitBare("Checking workspace status...");
69-
const loginInfo = await callApi(API.loginInfo);
70-
const spaceInited = loginInfo?.spaceInited === true;
89+
const loginResp = await callApi(API.loginInfo);
90+
const loginData = extractData(loginResp);
91+
const spaceInited = loginData?.spaceInited === true;
7192

7293
// Step 2: Init space if needed
7394
if (!spaceInited) {
@@ -90,7 +111,8 @@ async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"):
90111
let buyResult: string | undefined;
91112
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
92113
const resp = await call(API.queryBuyResult);
93-
buyResult = resp?.result;
114+
const data = extractData(resp);
115+
buyResult = typeof data === "string" ? data : data?.result;
94116
if (buyResult !== "buying") break;
95117
if (i === 0) emitBare("Service activation in progress, polling...");
96118
await sleep(POLL_INTERVAL_MS);
@@ -108,43 +130,48 @@ async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"):
108130
await checkAndActivateCommodities(call, format);
109131
}
110132

133+
function extractCommodities(resp: any): CommodityItem[] {
134+
const data = extractData(resp);
135+
return Array.isArray(data) ? data : [];
136+
}
137+
111138
async function checkAndActivateCommodities(call: ApiCall, format: "text" | "json"): Promise<void> {
112139
const resp = await call(API.commodityOrderInfo);
113-
const items: CommodityItem[] = resp?.result ?? [];
140+
const items = extractCommodities(resp);
114141

115142
const overdue = items.filter((c) => c.status === 11);
116143
if (overdue.length > 0) {
117144
emitBare("Warning: Some services are overdue:");
118-
for (const c of overdue) emitBare(` - ${c.commodity}`);
145+
for (const c of overdue) emitBare(` - ${c.commodityCode}`);
119146
}
120147

121148
const notActivated = items.filter((c) => c.status === 1);
122149
if (notActivated.length > 0) {
123-
emitBare("Activating postpaid services...");
150+
emitBare(`Activating ${notActivated.length} postpaid services...`);
124151
await call(API.buyCommodity);
125152
await pollCommoditiesUntilActive(call, format);
126153
return;
127154
}
128155

129156
const active = items.filter((c) => c.status === 10);
130-
emitResult({ status: "ready", activeServices: active.map((c) => c.commodity) }, format);
157+
emitResult({ status: "ready", activeServices: active.map((c) => c.commodityCode) }, format);
131158
}
132159

133160
async function pollCommoditiesUntilActive(call: ApiCall, format: "text" | "json"): Promise<void> {
134161
emitBare("Waiting for services to activate...");
135162
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
136163
const resp = await call(API.commodityOrderInfo);
137-
const items: CommodityItem[] = resp?.result ?? [];
164+
const items = extractCommodities(resp);
138165

139166
const pending = items.filter((c) => c.status !== 10 && c.status !== 11);
140167
if (pending.length === 0) {
141168
const overdue = items.filter((c) => c.status === 11);
142169
if (overdue.length > 0) {
143170
emitBare("Warning: Some services are overdue:");
144-
for (const c of overdue) emitBare(` - ${c.commodity}`);
171+
for (const c of overdue) emitBare(` - ${c.commodityCode}`);
145172
}
146173
const active = items.filter((c) => c.status === 10);
147-
emitResult({ status: "ready", activeServices: active.map((c) => c.commodity) }, format);
174+
emitResult({ status: "ready", activeServices: active.map((c) => c.commodityCode) }, format);
148175
return;
149176
}
150177
await sleep(POLL_INTERVAL_MS);

packages/core/src/auth/refresh-token.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ export async function generateCLIAccessToken(opts: {
3030
baseUrl: string;
3131
accessKeyId: string;
3232
accessKeySecret: string;
33+
securityToken?: string;
3334
}): Promise<any> {
34-
const { identity, settings, baseUrl, accessKeyId, accessKeySecret } = opts;
35+
const { identity, settings, baseUrl, accessKeyId, accessKeySecret, securityToken } = opts;
3536

3637
const client = new Client({
3738
identity,
3839
settings,
3940
baseUrl,
40-
openApiCred: { accessKeyId, accessKeySecret, source: "flag" },
41+
openApiCred: { accessKeyId, accessKeySecret, securityToken, source: "flag" },
4142
});
4243

4344
const host = modelStudioHost(baseUrl);

packages/core/src/auth/resolver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function resolveOpenApi(s: ResolutionSources): OpenApiCredential {
5555
s.flags.accessKeyId,
5656
s.flags.accessKeySecret,
5757
s.flags.accessKeyId !== undefined || s.flags.accessKeySecret !== undefined,
58+
s.flags.securityToken,
5859
);
5960
if (flagCred) return flagCred;
6061

@@ -66,6 +67,7 @@ export function resolveOpenApi(s: ResolutionSources): OpenApiCredential {
6667
trimNonEmpty(s.env.ALIBABA_CLOUD_ACCESS_KEY_ID) ||
6768
trimNonEmpty(s.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET),
6869
),
70+
s.env.ALIBABA_CLOUD_SECURITY_TOKEN,
6971
);
7072
if (envCred) return envCred;
7173

@@ -74,6 +76,7 @@ export function resolveOpenApi(s: ResolutionSources): OpenApiCredential {
7476
s.file.access_key_id,
7577
s.file.access_key_secret,
7678
Boolean(s.file.access_key_id || s.file.access_key_secret),
79+
s.file.security_token,
7780
);
7881
if (configCred) return configCred;
7982

@@ -89,6 +92,7 @@ function resolveOpenApiPair(
8992
rawAccessKeyId: string | undefined,
9093
rawAccessKeySecret: string | undefined,
9194
provided: boolean,
95+
rawSecurityToken?: string,
9296
): OpenApiCredential | undefined {
9397
if (!provided) return undefined;
9498

@@ -103,7 +107,8 @@ function resolveOpenApiPair(
103107
);
104108
}
105109

106-
return { accessKeyId, accessKeySecret, source };
110+
const securityToken = trimNonEmpty(rawSecurityToken);
111+
return { accessKeyId, accessKeySecret, securityToken, source };
107112
}
108113

109114
function trimNonEmpty(value: string | undefined): string | undefined {

packages/core/src/auth/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type AuthPersistPatch = Pick<
1717
| "access_token"
1818
| "access_key_id"
1919
| "access_key_secret"
20+
| "security_token"
2021
| "base_url"
2122
| "console_site"
2223
| "console_region"

packages/core/src/auth/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ConsoleCredential {
2222
export interface OpenApiCredential {
2323
accessKeyId: string;
2424
accessKeySecret: string;
25+
securityToken?: string;
2526
source: CredentialSource;
2627
}
2728

packages/core/src/client/acs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type AcsQueryParams = Record<string, string | string[] | undefined>;
55
export interface AcsSignConfig {
66
accessKeyId: string;
77
accessKeySecret: string;
8+
securityToken?: string;
89
action: string;
910
version: string;
1011
body: string;
@@ -49,6 +50,7 @@ export function signAcsRequest(cfg: AcsSignConfig): Record<string, string> {
4950
"x-acs-content-sha256": hashedBody,
5051
"content-type": "application/json",
5152
};
53+
if (cfg.securityToken) headers["x-acs-security-token"] = cfg.securityToken;
5254

5355
const signedHeaderKeys = Object.keys(headers)
5456
.filter((k) => k === "host" || k === "content-type" || k.startsWith("x-acs-"))

packages/core/src/client/client.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,15 @@ export class Client {
117117
if (!this.deps.consoleCred) {
118118
throw new BailianError("This command needs a console access token.", ExitCode.AUTH);
119119
}
120+
const gwOpts = { api, data };
121+
const { timeout } = this.deps.settings;
120122
try {
121-
return (await callConsoleGateway(this.deps.consoleCred, this.deps.settings.timeout, {
122-
api,
123-
data,
124-
})) as T;
123+
return (await callConsoleGateway(
124+
this.deps.consoleCred,
125+
timeout,
126+
gwOpts,
127+
this.deps.settings,
128+
)) as T;
125129
} catch (err) {
126130
if (
127131
!(err instanceof BailianError) ||
@@ -138,8 +142,9 @@ export class Client {
138142
if (!newToken) throw err;
139143
return (await callConsoleGateway(
140144
{ ...this.deps.consoleCred, token: newToken },
141-
this.deps.settings.timeout,
142-
{ api, data },
145+
timeout,
146+
gwOpts,
147+
this.deps.settings,
143148
)) as T;
144149
}
145150
}
@@ -151,6 +156,7 @@ export class Client {
151156
const headers = signAcsRequest({
152157
accessKeyId: cred.accessKeyId,
153158
accessKeySecret: cred.accessKeySecret,
159+
securityToken: cred.securityToken,
154160
action: opts.action,
155161
version: opts.version,
156162
body: "",

packages/core/src/config/schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface ConfigFile {
2222
access_key_id?: string;
2323
/** Alibaba Cloud OpenAPI AccessKey secret from `bl auth login --open-api`. */
2424
access_key_secret?: string;
25+
/** Alibaba Cloud STS Security Token (optional, for temporary credentials). */
26+
security_token?: string;
2527
base_url?: string;
2628
/**
2729
* Dedicated base URL for the intent-detect model (tongyi-intent-detect-v3).
@@ -83,6 +85,8 @@ export function parseConfigFile(raw: unknown): ConfigFile {
8385
obj.openapi_access_key_secret.length > 0
8486
)
8587
out.access_key_secret = obj.openapi_access_key_secret;
88+
if (typeof obj.security_token === "string" && obj.security_token.length > 0)
89+
out.security_token = obj.security_token;
8690
if (typeof obj.base_url === "string" && isHttpUrl(obj.base_url)) out.base_url = obj.base_url;
8791
if (typeof obj.intent_detect_base_url === "string" && isHttpUrl(obj.intent_detect_base_url))
8892
out.intent_detect_base_url = obj.intent_detect_base_url;

packages/core/src/console/gateway.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export async function callConsoleGateway(
100100
target: ConsoleGatewayTarget,
101101
timeoutSec: number,
102102
{ api, data }: ConsoleGatewayRequest,
103+
settings?: Pick<Settings, "verbose">,
103104
): Promise<unknown> {
104105
const resolved = resolveGateway(target.region, target.site);
105106
const gatewayBase = `https://${resolved.csGateway}`;
@@ -115,15 +116,21 @@ export async function callConsoleGateway(
115116
};
116117
if (target.token) headers.Authorization = `Bearer ${target.token}`;
117118

118-
const res = await fetch(
119-
`${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`,
120-
{
121-
method: "POST",
122-
headers,
123-
body: body.toString(),
124-
signal: AbortSignal.timeout(timeoutMs),
125-
},
126-
);
119+
const endpoint = `${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`;
120+
if (settings?.verbose) {
121+
process.stderr.write(`> POST ${endpoint}\n`);
122+
}
123+
124+
const res = await fetch(endpoint, {
125+
method: "POST",
126+
headers,
127+
body: body.toString(),
128+
signal: AbortSignal.timeout(timeoutMs),
129+
});
130+
131+
if (settings?.verbose) {
132+
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
133+
}
127134

128135
if (!res.ok) {
129136
const t = await res.text().catch(() => "");
@@ -135,6 +142,9 @@ export async function callConsoleGateway(
135142
}
136143

137144
const json = (await res.json()) as Record<string, unknown>;
145+
if (settings?.verbose) {
146+
process.stderr.write(`< ${JSON.stringify(json)}\n`);
147+
}
138148

139149
const innerData = json.data as Record<string, unknown> | undefined;
140150
if (innerData?.success === false && innerData.errorCode) {

0 commit comments

Comments
 (0)