@cloudbase/toolbox 0.7.20 → 0.7.21-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,10 @@
1
+ import { LocalStore } from '../localstore';
1
2
  import { Credential, WebAuthCredential, RequestConfig } from '../types';
3
+ export declare const DEFAULT_CLIENT_ID = "cloudbase-cli";
4
+ export declare const configStore: LocalStore<{
5
+ customOAuthEndpoint?: string;
6
+ client_id?: string;
7
+ }>;
2
8
  export declare function checkAuth(credential: Credential, options?: RequestConfig): Promise<any>;
3
9
  export declare function resolveCredential(data: Partial<Credential> & Partial<WebAuthCredential>): Credential;
4
10
  export declare function resolveWebCredential(credential: Credential): WebAuthCredential;
@@ -9,9 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.resolveWebCredential = exports.resolveCredential = exports.checkAuth = void 0;
12
+ exports.resolveWebCredential = exports.resolveCredential = exports.checkAuth = exports.configStore = exports.DEFAULT_CLIENT_ID = void 0;
13
13
  const cloud_api_1 = require("@cloudbase/cloud-api");
14
14
  const config_1 = require("../config");
15
+ const localstore_1 = require("../localstore");
16
+ exports.DEFAULT_CLIENT_ID = 'cloudbase-cli';
17
+ exports.configStore = new localstore_1.LocalStore({}, 'config');
15
18
  // 调用 CheckTcbService 接口,检查密钥是否有效
16
19
  // 相比 DescribeEnvs,该接口不会返回环境列表,仅验证登录态,性能更好
17
20
  function checkAuth(credential, options = {}) {
@@ -39,7 +42,7 @@ function checkAuth(credential, options = {}) {
39
42
  exports.checkAuth = checkAuth;
40
43
  // 兼容解析旧的登录态
41
44
  function resolveCredential(data) {
42
- let { secretId, secretKey, token, accessTokenExpired, tmpSecretId, tmpSecretKey, tmpToken, tmpExpired, expired, authTime, refreshToken, uin, hash, envId } = data;
45
+ let { secretId, secretKey, token, accessTokenExpired, tmpSecretId, tmpSecretKey, tmpToken, tmpExpired, expired, authTime, refreshToken, uin, hash, envId, envIds, envList, envBillingInfoList } = data;
43
46
  // 兼容旧的登录态信息
44
47
  token = token || tmpToken;
45
48
  secretId = secretId || tmpSecretId;
@@ -55,12 +58,15 @@ function resolveCredential(data) {
55
58
  refreshToken,
56
59
  uin,
57
60
  hash,
58
- envId
61
+ envId,
62
+ envIds,
63
+ envList,
64
+ envBillingInfoList
59
65
  };
60
66
  }
61
67
  exports.resolveCredential = resolveCredential;
62
68
  function resolveWebCredential(credential) {
63
- const { secretId, secretKey, token, accessTokenExpired, expired, authTime, refreshToken, uin, hash, envId } = credential;
69
+ const { secretId, secretKey, token, accessTokenExpired, expired, authTime, refreshToken, uin, hash, envId, envIds, envList, envBillingInfoList } = credential;
64
70
  const webCredential = {
65
71
  tmpSecretId: secretId,
66
72
  tmpSecretKey: secretKey,
@@ -71,7 +77,10 @@ function resolveWebCredential(credential) {
71
77
  refreshToken,
72
78
  uin,
73
79
  hash,
74
- envId
80
+ envId,
81
+ envIds,
82
+ envList,
83
+ envBillingInfoList
75
84
  };
76
85
  return webCredential;
77
86
  }
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  Object.defineProperty(exports, "__esModule", { value: true });
12
23
  exports.getCredentialWithoutCheck = exports.checkAndGetCredential = exports.isCamRefused = exports.refreshTmpToken = exports.getCredentialData = exports.ENV_NAME = exports.authStore = void 0;
13
24
  const cloud_api_1 = require("@cloudbase/cloud-api");
@@ -67,12 +78,53 @@ function refreshTmpToken(metaData) {
67
78
  const mac = yield (0, system_1.getMacAddress)();
68
79
  const hash = (0, coding_1.md5Encoding)(mac);
69
80
  metaData.hash = hash;
81
+ // 读取自定义 OAuth 配置
82
+ const customOAuthEndpoint = yield common_1.configStore.get('customOAuthEndpoint');
83
+ if (customOAuthEndpoint) {
84
+ // ===== 自定义模式 =====
85
+ const clientId = (yield common_1.configStore.get('client_id')) || common_1.DEFAULT_CLIENT_ID;
86
+ const body = {
87
+ grant_type: metaData.isLogout ? 'revoke_token' : 'refresh_token',
88
+ refresh_token: metaData.refreshToken,
89
+ client_id: clientId,
90
+ };
91
+ const res = yield async_1.AsyncMerge.merge(() => (0, cloud_api_1.fetch)(`${customOAuthEndpoint}/token`, {
92
+ method: 'POST',
93
+ timeout: 15000,
94
+ body: JSON.stringify(body),
95
+ headers: { 'Content-Type': 'application/json' }
96
+ }, (0, system_1.getProxy)()), 'refreshToken', {
97
+ maxAge: 5000
98
+ });
99
+ // 自定义模式错误响应:{ error, error_description }
100
+ if (res.error) {
101
+ throw new error_1.CloudBaseError(res.error_description || res.error, {
102
+ action: metaData.isLogout ? 'RevokeToken' : 'RefreshAccessToken',
103
+ code: res.error,
104
+ });
105
+ }
106
+ // 登出(revoke_token)成功,无需返回凭证
107
+ if (metaData.isLogout) {
108
+ return res;
109
+ }
110
+ // 刷新 token 成功响应直接是 WebAuthCredential(无 code/data 包装)
111
+ if (!res || !res.tmpSecretId) {
112
+ throw new error_1.CloudBaseError('Token 刷新失败,可能是网络访问异常,请尝试重试');
113
+ }
114
+ if (metaData.envId) {
115
+ res.envId = metaData.envId;
116
+ }
117
+ return res;
118
+ }
119
+ // ===== 标准模式 =====
70
120
  const credential = (0, common_1.resolveWebCredential)(metaData);
121
+ // 发送请求时不带 envIds/envList/envBillingInfoList
122
+ const { envIds: _a, envList: _b, envBillingInfoList: _c } = credential, requestBody = __rest(credential, ["envIds", "envList", "envBillingInfoList"]);
71
123
  const res = yield async_1.AsyncMerge.merge(() => (0, cloud_api_1.fetch)(refreshTokenUrl, {
72
124
  method: 'POST',
73
125
  // 超时时间:15S
74
126
  timeout: 15000,
75
- body: JSON.stringify(credential),
127
+ body: JSON.stringify(requestBody),
76
128
  headers: { 'Content-Type': 'application/json' }
77
129
  }, (0, system_1.getProxy)()), 'refreshToken', {
78
130
  maxAge: 5000
@@ -1,4 +1,4 @@
1
- import { DeviceFlowOptions } from './oauth';
1
+ import { DeviceFlowOptions, DeviceCodeInfo, AuthTokenByDeviceCodeOptions } from './oauth';
2
2
  import { Credential, RequestConfig } from '../types';
3
3
  export * from './common';
4
4
  export * from './credential';
@@ -45,6 +45,8 @@ export interface WebAuthOptions {
45
45
  onDeviceCode?: DeviceFlowOptions['onDeviceCode'];
46
46
  /** 自定义 OAuth API 请求的 endpoint */
47
47
  getOAuthEndpoint?: DeviceFlowOptions['getOAuthEndpoint'];
48
+ /** 返回即将打开的授权链接 */
49
+ onAuthUrl?: (url: string) => void;
48
50
  /** 静默模式,不打印任何日志 */
49
51
  silent?: boolean;
50
52
  /**
@@ -70,6 +72,10 @@ export interface LoginByApiSecretOptions {
70
72
  */
71
73
  additionalCredentialFields?: Omit<Credential, 'secretId' | 'secretKey' | 'token'>;
72
74
  }
75
+ export interface StartDeviceFlowOptions extends DeviceFlowOptions {
76
+ noBrowser?: boolean;
77
+ }
78
+ export type CompleteDeviceFlowOptions = AuthTokenByDeviceCodeOptions;
73
79
  export declare class AuthSupervisor {
74
80
  static instance: AuthSupervisor;
75
81
  /**
@@ -92,6 +98,8 @@ export declare class AuthSupervisor {
92
98
  * @returns credential
93
99
  */
94
100
  loginByWebAuth(options?: WebAuthOptions): Promise<Credential>;
101
+ startDeviceFlow(options?: StartDeviceFlowOptions): Promise<DeviceCodeInfo>;
102
+ completeDeviceFlow(options: CompleteDeviceFlowOptions): Promise<Credential>;
95
103
  /**
96
104
  * 通过 API Secret 登录,支持临时秘钥
97
105
  * @param secretId
@@ -102,6 +110,8 @@ export declare class AuthSupervisor {
102
110
  */
103
111
  loginByApiSecret(secretId?: string, secretKey?: string, token?: string, opts?: LoginByApiSecretOptions): Promise<Credential>;
104
112
  logout(): Promise<void>;
113
+ private finalizeWebAuthCredential;
114
+ private updateCredentialCache;
105
115
  private isCacheExpire;
106
116
  }
107
117
  /**
package/lib/auth/index.js CHANGED
@@ -22,6 +22,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
22
22
  step((generator = generator.apply(thisArg, _arguments || [])).next());
23
23
  });
24
24
  };
25
+ var __rest = (this && this.__rest) || function (s, e) {
26
+ var t = {};
27
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
28
+ t[p] = s[p];
29
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
30
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
31
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
32
+ t[p[i]] = s[p[i]];
33
+ }
34
+ return t;
35
+ };
25
36
  Object.defineProperty(exports, "__esModule", { value: true });
26
37
  exports.AuthSupevisor = exports.AuthSupervisor = void 0;
27
38
  const web_auth_1 = require("./web-auth");
@@ -29,6 +40,7 @@ const oauth_1 = require("./oauth");
29
40
  const common_1 = require("./common");
30
41
  const credential_1 = require("./credential");
31
42
  const error_1 = require("../error");
43
+ const web_1 = require("../web");
32
44
  __exportStar(require("./common"), exports);
33
45
  __exportStar(require("./credential"), exports);
34
46
  __exportStar(require("./web-auth"), exports);
@@ -82,7 +94,7 @@ class AuthSupervisor {
82
94
  */
83
95
  loginByWebAuth(options = {}) {
84
96
  return __awaiter(this, void 0, void 0, function* () {
85
- const { getAuthUrl, throwError, noBrowser, callbackTimeout, flow = 'device', client_id, onDeviceCode, getOAuthEndpoint, silent, custom } = options;
97
+ const { getAuthUrl, throwError, noBrowser, callbackTimeout, flow = 'device', client_id, onDeviceCode, getOAuthEndpoint, onAuthUrl, silent, custom } = options;
86
98
  if (this.cacheCredential && this.needCache && !this.isCacheExpire()) {
87
99
  return this.cacheCredential;
88
100
  }
@@ -96,34 +108,36 @@ class AuthSupervisor {
96
108
  getAuthUrl,
97
109
  noBrowser,
98
110
  callbackTimeout,
99
- silent
111
+ silent,
112
+ onAuthUrl
100
113
  });
101
114
  }
102
115
  else {
103
116
  credential = yield (0, oauth_1.getAuthTokenByDeviceFlow)({ client_id, onDeviceCode, getOAuthEndpoint, getAuthUrl, silent, custom });
104
117
  }
105
- credential = (0, common_1.resolveCredential)(credential);
106
- try {
107
- yield (0, common_1.checkAuth)(credential, this.requestConfig);
108
- }
109
- catch (error) {
110
- if (throwError || this.throwError) {
111
- throw error;
112
- }
113
- return null;
118
+ return this.finalizeWebAuthCredential(credential, throwError);
119
+ });
120
+ }
121
+ startDeviceFlow(options = {}) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ const { noBrowser } = options, deviceFlowOptions = __rest(options, ["noBrowser"]);
124
+ const deviceCodeInfo = yield (0, oauth_1.getDeviceCodeByDeviceFlow)(deviceFlowOptions);
125
+ if (!noBrowser) {
126
+ yield (0, web_1.openUrl)(deviceCodeInfo.verification_uri);
114
127
  }
115
- // 通过 Web 登录时,本地要存储 tmpSecretId 形式,方式 CLI 登录失效
116
- const webCredential = (0, common_1.resolveWebCredential)(credential);
117
- yield credential_1.authStore.set('credential', webCredential);
118
- // 缓存处理转换后的 credential
119
- if (this.needCache && credential) {
120
- this.cacheCredential = credential;
121
- const { accessTokenExpired } = credential;
122
- this.cacheExpiredTime = accessTokenExpired
123
- ? Number(accessTokenExpired)
124
- : Date.now() + 3600 * 1000;
128
+ return deviceCodeInfo;
129
+ });
130
+ }
131
+ completeDeviceFlow(options) {
132
+ return __awaiter(this, void 0, void 0, function* () {
133
+ if (this.cacheCredential && this.needCache && !this.isCacheExpire()) {
134
+ return this.cacheCredential;
125
135
  }
126
- return credential;
136
+ let credential = yield (0, credential_1.checkAndGetCredential)(this.requestConfig);
137
+ if (credential)
138
+ return credential;
139
+ credential = yield (0, oauth_1.getAuthTokenByDeviceCode)(options);
140
+ return this.finalizeWebAuthCredential(credential);
127
141
  });
128
142
  }
129
143
  /**
@@ -200,6 +214,35 @@ class AuthSupervisor {
200
214
  }
201
215
  });
202
216
  }
217
+ finalizeWebAuthCredential(credential, throwError) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ // eslint-disable-next-line no-param-reassign
220
+ credential = (0, common_1.resolveCredential)(credential);
221
+ try {
222
+ yield (0, common_1.checkAuth)(credential, this.requestConfig);
223
+ }
224
+ catch (error) {
225
+ if (throwError || this.throwError) {
226
+ throw error;
227
+ }
228
+ return null;
229
+ }
230
+ const webCredential = (0, common_1.resolveWebCredential)(credential);
231
+ yield credential_1.authStore.set('credential', webCredential);
232
+ this.updateCredentialCache(credential);
233
+ return credential;
234
+ });
235
+ }
236
+ updateCredentialCache(credential) {
237
+ if (!this.needCache || !credential) {
238
+ return;
239
+ }
240
+ this.cacheCredential = credential;
241
+ const { accessTokenExpired } = credential;
242
+ this.cacheExpiredTime = accessTokenExpired
243
+ ? Number(accessTokenExpired)
244
+ : Date.now() + 3600 * 1000;
245
+ }
203
246
  isCacheExpire() {
204
247
  const now = Date.now();
205
248
  this.cacheExpiredTime = this.cacheExpiredTime || 0;
@@ -1,4 +1,11 @@
1
1
  import { Credential } from "../types";
2
+ export interface DeviceCodeInfo {
3
+ device_code: string;
4
+ user_code: string;
5
+ verification_uri: string;
6
+ expires_in: number;
7
+ interval: number;
8
+ }
2
9
  export interface DeviceFlowOptions {
3
10
  client_id?: string;
4
11
  onDeviceCode?: (data: {
@@ -20,4 +27,22 @@ export interface DeviceFlowOptions {
20
27
  */
21
28
  custom?: boolean;
22
29
  }
30
+ export interface AuthTokenByDeviceCodeOptions {
31
+ device_code: string;
32
+ /** 初始轮询间隔 s,默认 5 */
33
+ interval?: number;
34
+ client_id?: string;
35
+ /** 自定义 OAuth API 请求的 endpoint */
36
+ getOAuthEndpoint?: DeviceFlowOptions['getOAuthEndpoint'];
37
+ /**
38
+ * 自定义授权流程模式
39
+ * 为 true 时,token 接口的返回值没有外层 { code, result, reqId } 包装,
40
+ * 直接就是 result 本体。
41
+ */
42
+ custom?: DeviceFlowOptions['custom'];
43
+ /** 最长等待时长 ms,默认 30000 */
44
+ timeout?: number;
45
+ }
46
+ export declare function getDeviceCodeByDeviceFlow(options?: DeviceFlowOptions): Promise<DeviceCodeInfo>;
47
+ export declare function getAuthTokenByDeviceCode(options: AuthTokenByDeviceCodeOptions): Promise<Credential>;
23
48
  export declare function getAuthTokenByDeviceFlow(options?: DeviceFlowOptions): Promise<Credential>;
package/lib/auth/oauth.js CHANGED
@@ -9,16 +9,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getAuthTokenByDeviceFlow = void 0;
12
+ exports.getAuthTokenByDeviceFlow = exports.getAuthTokenByDeviceCode = exports.getDeviceCodeByDeviceFlow = void 0;
13
13
  const net_1 = require("../net");
14
14
  const web_1 = require("../web");
15
15
  const error_1 = require("../error");
16
16
  const coding_1 = require("../coding");
17
17
  const system_1 = require("../system");
18
18
  const web_auth_1 = require("./web-auth");
19
+ const common_1 = require("./common");
19
20
  /** 默认国内站 应用侧可通过 getOAuthEndpoint 覆写 */
20
- const OAUTH_ENDPOINT = process.env.TCB_OAUTH_ENDPOINT || 'https://tcb-api.cloud.tencent.com/qcloud-tcb/v1/oauth';
21
- const DEFAULT_CLIENT_ID = 'cloudbase-toolbox';
21
+ const DEFAULT_OAUTH_ENDPOINT = process.env.TCB_OAUTH_ENDPOINT || 'https://tcb-api.cloud.tencent.com/qcloud-tcb/v1/oauth';
22
+ /**
23
+ * 解析 OAuth Endpoint,优先级:getOAuthEndpoint > customOAuthEndpoint > default
24
+ */
25
+ function resolveOAuthEndpoint(getOAuthEndpoint) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ if (getOAuthEndpoint) {
28
+ return getOAuthEndpoint(DEFAULT_OAUTH_ENDPOINT);
29
+ }
30
+ const customOAuthEndpoint = yield common_1.configStore.get('customOAuthEndpoint');
31
+ if (customOAuthEndpoint) {
32
+ return customOAuthEndpoint;
33
+ }
34
+ return DEFAULT_OAUTH_ENDPOINT;
35
+ });
36
+ }
22
37
  const POLL_ERROR_CODES = {
23
38
  AUTHORIZATION_PENDING: 'authorization_pending',
24
39
  SLOW_DOWN: 'slow_down',
@@ -27,12 +42,12 @@ const POLL_ERROR_CODES = {
27
42
  };
28
43
  const SUCCESS_CODE = 'NORMAL';
29
44
  function fetchDeviceCode(options = {}) {
30
- const { client_id = DEFAULT_CLIENT_ID, endpoint = OAUTH_ENDPOINT } = options;
45
+ const { client_id = common_1.DEFAULT_CLIENT_ID, endpoint = DEFAULT_OAUTH_ENDPOINT } = options;
31
46
  return (0, net_1.postFetch)(`${endpoint}/device/code`, { client_id });
32
47
  }
33
48
  function fetchPollToken(options) {
34
49
  return __awaiter(this, void 0, void 0, function* () {
35
- const { device_code, client_id = DEFAULT_CLIENT_ID, endpoint = OAUTH_ENDPOINT } = options;
50
+ const { device_code, client_id = common_1.DEFAULT_CLIENT_ID, endpoint = DEFAULT_OAUTH_ENDPOINT } = options;
36
51
  const mac = yield (0, system_1.getMacAddress)();
37
52
  return (0, net_1.postFetch)(`${endpoint}/token`, {
38
53
  device_code,
@@ -51,6 +66,17 @@ function sleep(ms) {
51
66
  setTimeout(resolve, ms);
52
67
  });
53
68
  }
69
+ function buildVerificationUri(deviceCodeData, options) {
70
+ const { custom, getAuthUrl } = options;
71
+ if (custom) {
72
+ const rawUri = deviceCodeData.verification_uri || '';
73
+ return getAuthUrl ? getAuthUrl(rawUri) : rawUri;
74
+ }
75
+ const defaultVerificationUri = `${web_auth_1.CLI_AUTH_BASE_URL}#/cli-auth`;
76
+ // from=cli 后续考虑改用 client_id,因为 toolbox 作为公共依赖,可能被不同 client 所引用
77
+ const rawVerificationUri = `${defaultVerificationUri}?user_code=${deviceCodeData.user_code}&flow=device&from=cli`;
78
+ return getAuthUrl ? getAuthUrl(rawVerificationUri) : rawVerificationUri;
79
+ }
54
80
  /**
55
81
  * 将接口原始返回值归一化为裸数据。
56
82
  * - 标准模式:从 { code, result, reqId } 中解包,校验 code
@@ -86,43 +112,24 @@ function unwrapTokenResponse(raw, custom) {
86
112
  }
87
113
  return { data: resp.result, reqId: resp.reqId };
88
114
  }
89
- function getAuthTokenByDeviceFlow(options = {}) {
115
+ function getDeviceCodeByDeviceFlow(options = {}) {
90
116
  return __awaiter(this, void 0, void 0, function* () {
91
- const { client_id, onDeviceCode, getOAuthEndpoint, getAuthUrl, silent, custom } = options;
92
- const endpoint = getOAuthEndpoint ? getOAuthEndpoint(OAUTH_ENDPOINT) : OAUTH_ENDPOINT;
93
- // ---- 获取 device code ----
117
+ const { client_id, getOAuthEndpoint, getAuthUrl, custom } = options;
118
+ const endpoint = yield resolveOAuthEndpoint(getOAuthEndpoint);
94
119
  const deviceCodeRaw = yield fetchDeviceCode({ client_id, endpoint });
95
120
  const { data: deviceCodeData } = unwrapDeviceCodeResponse(deviceCodeRaw, !!custom);
96
- const { device_code, user_code, expires_in, interval } = deviceCodeData;
97
- // ---- 构造 verification_uri ----
98
- let verification_uri;
99
- if (custom) {
100
- // custom 模式直接使用接口返回的 verification_uri
101
- const rawUri = deviceCodeData.verification_uri || '';
102
- verification_uri = getAuthUrl ? getAuthUrl(rawUri) : rawUri;
103
- }
104
- else {
105
- // 标准模式由客户端拼接 verification_uri
106
- const defaultVerificationUri = `${web_auth_1.CLI_AUTH_BASE_URL}#/cli-auth`;
107
- // from=cli 后续考虑改用 client_id,因为 toolbox 作为公共依赖,可能被不同 client 所引用
108
- const rawVerificationUri = `${defaultVerificationUri}?user_code=${user_code}&from=cli&flow=device`;
109
- verification_uri = getAuthUrl ? getAuthUrl(rawVerificationUri) : rawVerificationUri;
110
- }
111
- if (!silent) {
112
- console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
113
- console.log(`\n${verification_uri}`);
114
- console.log(`\n用户码: ${user_code}\n`);
115
- }
116
- // 尝试自动打开授权页面
117
- yield (0, web_1.openUrl)(verification_uri);
118
- if (onDeviceCode) {
119
- onDeviceCode({ user_code, verification_uri, device_code, expires_in });
120
- }
121
- // ---- 轮询 token ----
121
+ const verification_uri = buildVerificationUri(deviceCodeData, { custom, getAuthUrl });
122
+ return Object.assign(Object.assign({}, deviceCodeData), { verification_uri });
123
+ });
124
+ }
125
+ exports.getDeviceCodeByDeviceFlow = getDeviceCodeByDeviceFlow;
126
+ function getAuthTokenByDeviceCode(options) {
127
+ return __awaiter(this, void 0, void 0, function* () {
128
+ const { device_code, interval = 5, client_id, getOAuthEndpoint, custom, timeout = 30000 } = options;
129
+ const endpoint = yield resolveOAuthEndpoint(getOAuthEndpoint);
122
130
  let pollInterval = interval;
123
- const deadline = Date.now() + expires_in * 1000;
124
- while (Date.now() < deadline) {
125
- yield sleep(pollInterval * 1000);
131
+ const deadline = timeout > 0 ? Date.now() + timeout : 0;
132
+ while (!deadline || Date.now() < deadline) {
126
133
  let tokenData;
127
134
  let reqId;
128
135
  try {
@@ -134,14 +141,23 @@ function getAuthTokenByDeviceFlow(options = {}) {
134
141
  throw e;
135
142
  throw new error_1.CloudBaseError('Device Flow 轮询请求失败', { original: e });
136
143
  }
137
- // 服务端返回业务错误(携带 error 字段)
138
144
  if (tokenData && 'error' in tokenData) {
139
145
  const { error, error_description } = tokenData;
140
146
  if (error === POLL_ERROR_CODES.AUTHORIZATION_PENDING) {
147
+ const nextWaitMs = pollInterval * 1000;
148
+ if (deadline && Date.now() + nextWaitMs >= deadline) {
149
+ break;
150
+ }
151
+ yield sleep(nextWaitMs);
141
152
  continue;
142
153
  }
143
154
  if (error === POLL_ERROR_CODES.SLOW_DOWN) {
144
155
  pollInterval += 5;
156
+ const nextWaitMs = pollInterval * 1000;
157
+ if (deadline && Date.now() + nextWaitMs >= deadline) {
158
+ break;
159
+ }
160
+ yield sleep(nextWaitMs);
145
161
  continue;
146
162
  }
147
163
  if (error === POLL_ERROR_CODES.EXPIRED_TOKEN) {
@@ -155,13 +171,47 @@ function getAuthTokenByDeviceFlow(options = {}) {
155
171
  requestId: reqId,
156
172
  });
157
173
  }
158
- // 成功拿到 Credential
159
174
  if (tokenData) {
160
175
  return tokenData;
161
176
  }
162
177
  throw new error_1.CloudBaseError('Device Flow 授权失败:返回数据异常', { requestId: reqId });
163
178
  }
164
- throw new error_1.CloudBaseError('授权超时,用户未在有效期内完成授权,请重试');
179
+ throw new error_1.CloudBaseError('授权尚未完成,请确认已完成授权后重试', {
180
+ code: POLL_ERROR_CODES.AUTHORIZATION_PENDING,
181
+ });
182
+ });
183
+ }
184
+ exports.getAuthTokenByDeviceCode = getAuthTokenByDeviceCode;
185
+ function getAuthTokenByDeviceFlow(options = {}) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ const { client_id, onDeviceCode, getOAuthEndpoint, getAuthUrl, silent, custom } = options;
188
+ const { device_code, user_code, verification_uri, expires_in, interval } = yield getDeviceCodeByDeviceFlow({ client_id, getOAuthEndpoint, getAuthUrl, custom });
189
+ if (!silent) {
190
+ console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
191
+ console.log(`\n${verification_uri}`);
192
+ console.log(`\n用户码: ${user_code}\n`);
193
+ }
194
+ // 尝试自动打开授权页面
195
+ yield (0, web_1.openUrl)(verification_uri);
196
+ if (onDeviceCode) {
197
+ onDeviceCode({ user_code, verification_uri, device_code, expires_in });
198
+ }
199
+ try {
200
+ return yield getAuthTokenByDeviceCode({
201
+ device_code,
202
+ interval,
203
+ client_id,
204
+ getOAuthEndpoint,
205
+ custom,
206
+ timeout: expires_in * 1000
207
+ });
208
+ }
209
+ catch (e) {
210
+ if (e instanceof error_1.CloudBaseError && e.code === POLL_ERROR_CODES.AUTHORIZATION_PENDING) {
211
+ throw new error_1.CloudBaseError('授权超时,用户未在有效期内完成授权,请重试');
212
+ }
213
+ throw e;
214
+ }
165
215
  });
166
216
  }
167
217
  exports.getAuthTokenByDeviceFlow = getAuthTokenByDeviceFlow;
@@ -6,4 +6,6 @@ export declare function getAuthTokenFromWeb(options?: {
6
6
  callbackTimeout?: number;
7
7
  /** 静默模式,不打印任何日志 */
8
8
  silent?: boolean;
9
+ /** 返回即将打开的授权链接 */
10
+ onAuthUrl?: (url: string) => void;
9
11
  }): Promise<Credential>;
@@ -18,7 +18,7 @@ exports.CLI_AUTH_BASE_URL = 'https://tcb.cloud.tencent.com/dev';
18
18
  // 打开云开发控制台,获取授权
19
19
  function getAuthTokenFromWeb(options = {}) {
20
20
  return __awaiter(this, void 0, void 0, function* () {
21
- const { getAuthUrl, noBrowser, callbackTimeout, silent } = options;
21
+ const { getAuthUrl, noBrowser, callbackTimeout, silent, onAuthUrl } = options;
22
22
  const mac = yield (0, system_1.getMacAddress)();
23
23
  const os = (0, system_1.getOSInfo)();
24
24
  const macHash = (0, coding_1.md5Encoding)(mac);
@@ -45,7 +45,8 @@ function getAuthTokenFromWeb(options = {}) {
45
45
  }, 'login', {
46
46
  noBrowser,
47
47
  callbackTimeout,
48
- silent
48
+ silent,
49
+ onUrl: onAuthUrl
49
50
  });
50
51
  const credential = (0, common_1.resolveCredential)(query);
51
52
  return credential;
package/lib/types.d.ts CHANGED
@@ -1,3 +1,88 @@
1
+ export interface EnvInfo {
2
+ EnvId: string;
3
+ PackageType?: string;
4
+ EnvType?: string;
5
+ IsDauPackage?: boolean;
6
+ Source?: string;
7
+ Alias?: string;
8
+ Status?: string;
9
+ PayMode?: string;
10
+ Tags?: Record<string, string>[];
11
+ PackageName?: string;
12
+ IsAutoDegrade?: boolean;
13
+ EnvChannel?: string;
14
+ Region?: string;
15
+ IsDefault?: boolean;
16
+ PackageId?: string;
17
+ CreateTime?: string;
18
+ UpdateTime?: string;
19
+ Databases?: {
20
+ InstanceId: string;
21
+ Region: string;
22
+ Status: string;
23
+ UpdateTime: string;
24
+ }[];
25
+ Storages?: {
26
+ Region: string;
27
+ Bucket: string;
28
+ CdnDomain: string;
29
+ AppId: string;
30
+ }[];
31
+ Functions?: {
32
+ Namespace: string;
33
+ Region: string;
34
+ }[];
35
+ LogServices?: {
36
+ LogsetName: string;
37
+ LogsetId: string;
38
+ TopicName: string;
39
+ TopicId: string;
40
+ Region: string;
41
+ }[];
42
+ StaticStorages?: {
43
+ Type: string;
44
+ Bucket: string;
45
+ Region: string;
46
+ Status: string;
47
+ }[];
48
+ CustomLogServices?: {
49
+ ClsTopicId: string;
50
+ ClsRegion: string;
51
+ ClsLogsetId: string;
52
+ CreateTime: string;
53
+ }[];
54
+ }
55
+ export interface OrderInfo {
56
+ TranId?: string;
57
+ PackageId?: string;
58
+ TranType?: string;
59
+ TranStatus?: string;
60
+ UpdateTime?: string;
61
+ CreateTime?: string;
62
+ PayMode?: string;
63
+ ExtensionId?: string;
64
+ ResourceReady?: string;
65
+ Flag?: string;
66
+ ReqBody?: string;
67
+ }
68
+ export interface EnvBillingInfoItem {
69
+ EnvId: string;
70
+ PackageId?: string;
71
+ PackageName?: string;
72
+ IsAutoRenew?: boolean;
73
+ Status?: string;
74
+ PayMode?: string;
75
+ IsolatedTime?: string;
76
+ ExpireTime?: string;
77
+ CreateTime?: string;
78
+ UpdateTime?: string;
79
+ FreeQuota?: string;
80
+ PaymentChannel?: string;
81
+ ExtPackageType?: string;
82
+ OrderInfo?: OrderInfo;
83
+ IsAlwaysFree?: boolean;
84
+ EnableOverrun?: boolean;
85
+ }
1
86
  export interface Credential {
2
87
  secretId: string;
3
88
  secretKey: string;
@@ -9,6 +94,12 @@ export interface Credential {
9
94
  uin?: string;
10
95
  hash?: string;
11
96
  envId?: string;
97
+ /** 该用户可用的环境 ID 列表(有序,与 envList 顺序一致) */
98
+ envIds?: string[];
99
+ /** 环境批量详情,结构同 DescribeEnvs 接口返回的 EnvList */
100
+ envList?: EnvInfo[];
101
+ /** 环境计费信息列表,结构同 DescribeBillingInfo 接口返回的 EnvBillingInfoList */
102
+ envBillingInfoList?: EnvBillingInfoItem[];
12
103
  }
13
104
  export interface WebAuthCredential {
14
105
  tmpSecretId: string;
@@ -21,6 +112,12 @@ export interface WebAuthCredential {
21
112
  uin?: string;
22
113
  hash?: string;
23
114
  envId?: string;
115
+ /** 该用户可用的环境 ID 列表(有序,与 envList 顺序一致) */
116
+ envIds?: string[];
117
+ /** 环境批量详情,结构同 DescribeEnvs 接口返回的 EnvList */
118
+ envList?: EnvInfo[];
119
+ /** 环境计费信息列表,结构同 DescribeBillingInfo 接口返回的 EnvBillingInfoList */
120
+ envBillingInfoList?: EnvBillingInfoItem[];
24
121
  }
25
122
  export interface RequestConfig {
26
123
  proxy?: string;
package/lib/web/web.d.ts CHANGED
@@ -6,8 +6,10 @@ export type CheckFn = (query: IQuery) => Promise<void>;
6
6
  export interface GetDataFromWebOptions {
7
7
  noBrowser?: boolean;
8
8
  callbackTimeout?: number;
9
- /** 静默模式,不打印任何日志,也不自动打开浏览器 */
9
+ /** 静默模式,不打印任何日志 */
10
10
  silent?: boolean;
11
+ /** 返回即将打开的授权链接 */
12
+ onUrl?: (url: string) => void;
11
13
  }
12
14
  export declare function isTruthyFlag(value?: string): boolean;
13
15
  export declare function openUrl(url: string): Promise<boolean>;
package/lib/web/web.js CHANGED
@@ -124,10 +124,14 @@ function getDataFromWeb(getUrl, type, options = {}) {
124
124
  const noBrowser = (_a = options.noBrowser) !== null && _a !== void 0 ? _a : isTruthyFlag(process.env.TCB_NO_BROWSER);
125
125
  const callbackTimeout = (_b = options.callbackTimeout) !== null && _b !== void 0 ? _b : 180000;
126
126
  const silent = (_c = options.silent) !== null && _c !== void 0 ? _c : false;
127
+ const onUrl = options.onUrl;
127
128
  if (!Number.isFinite(callbackTimeout) || callbackTimeout <= 0) {
128
129
  throw new error_1.CloudBaseError('callbackTimeout must be a positive number');
129
130
  }
130
131
  const url = getUrl(port);
132
+ if (onUrl) {
133
+ onUrl(url);
134
+ }
131
135
  if (!silent) {
132
136
  console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
133
137
  console.log(`\n${url}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/toolbox",
3
- "version": "0.7.20",
3
+ "version": "0.7.21-beta.1",
4
4
  "description": "The toolbox for cloudbase",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {