@cloudbase/toolbox 0.7.21-beta.0 → 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,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,7 +9,7 @@ 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");
@@ -66,6 +66,17 @@ function sleep(ms) {
66
66
  setTimeout(resolve, ms);
67
67
  });
68
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
+ }
69
80
  /**
70
81
  * 将接口原始返回值归一化为裸数据。
71
82
  * - 标准模式:从 { code, result, reqId } 中解包,校验 code
@@ -101,43 +112,24 @@ function unwrapTokenResponse(raw, custom) {
101
112
  }
102
113
  return { data: resp.result, reqId: resp.reqId };
103
114
  }
104
- function getAuthTokenByDeviceFlow(options = {}) {
115
+ function getDeviceCodeByDeviceFlow(options = {}) {
105
116
  return __awaiter(this, void 0, void 0, function* () {
106
- const { client_id, onDeviceCode, getOAuthEndpoint, getAuthUrl, silent, custom } = options;
117
+ const { client_id, getOAuthEndpoint, getAuthUrl, custom } = options;
107
118
  const endpoint = yield resolveOAuthEndpoint(getOAuthEndpoint);
108
- // ---- 获取 device code ----
109
119
  const deviceCodeRaw = yield fetchDeviceCode({ client_id, endpoint });
110
120
  const { data: deviceCodeData } = unwrapDeviceCodeResponse(deviceCodeRaw, !!custom);
111
- const { device_code, user_code, expires_in, interval } = deviceCodeData;
112
- // ---- 构造 verification_uri ----
113
- let verification_uri;
114
- if (custom) {
115
- // custom 模式直接使用接口返回的 verification_uri
116
- const rawUri = deviceCodeData.verification_uri || '';
117
- verification_uri = getAuthUrl ? getAuthUrl(rawUri) : rawUri;
118
- }
119
- else {
120
- // 标准模式由客户端拼接 verification_uri
121
- const defaultVerificationUri = `${web_auth_1.CLI_AUTH_BASE_URL}#/cli-auth`;
122
- // from=cli 后续考虑改用 client_id,因为 toolbox 作为公共依赖,可能被不同 client 所引用
123
- const rawVerificationUri = `${defaultVerificationUri}?user_code=${user_code}&from=cli&flow=device`;
124
- verification_uri = getAuthUrl ? getAuthUrl(rawVerificationUri) : rawVerificationUri;
125
- }
126
- if (!silent) {
127
- console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
128
- console.log(`\n${verification_uri}`);
129
- console.log(`\n用户码: ${user_code}\n`);
130
- }
131
- // 尝试自动打开授权页面
132
- yield (0, web_1.openUrl)(verification_uri);
133
- if (onDeviceCode) {
134
- onDeviceCode({ user_code, verification_uri, device_code, expires_in });
135
- }
136
- // ---- 轮询 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);
137
130
  let pollInterval = interval;
138
- const deadline = Date.now() + expires_in * 1000;
139
- while (Date.now() < deadline) {
140
- yield sleep(pollInterval * 1000);
131
+ const deadline = timeout > 0 ? Date.now() + timeout : 0;
132
+ while (!deadline || Date.now() < deadline) {
141
133
  let tokenData;
142
134
  let reqId;
143
135
  try {
@@ -149,14 +141,23 @@ function getAuthTokenByDeviceFlow(options = {}) {
149
141
  throw e;
150
142
  throw new error_1.CloudBaseError('Device Flow 轮询请求失败', { original: e });
151
143
  }
152
- // 服务端返回业务错误(携带 error 字段)
153
144
  if (tokenData && 'error' in tokenData) {
154
145
  const { error, error_description } = tokenData;
155
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);
156
152
  continue;
157
153
  }
158
154
  if (error === POLL_ERROR_CODES.SLOW_DOWN) {
159
155
  pollInterval += 5;
156
+ const nextWaitMs = pollInterval * 1000;
157
+ if (deadline && Date.now() + nextWaitMs >= deadline) {
158
+ break;
159
+ }
160
+ yield sleep(nextWaitMs);
160
161
  continue;
161
162
  }
162
163
  if (error === POLL_ERROR_CODES.EXPIRED_TOKEN) {
@@ -170,13 +171,47 @@ function getAuthTokenByDeviceFlow(options = {}) {
170
171
  requestId: reqId,
171
172
  });
172
173
  }
173
- // 成功拿到 Credential
174
174
  if (tokenData) {
175
175
  return tokenData;
176
176
  }
177
177
  throw new error_1.CloudBaseError('Device Flow 授权失败:返回数据异常', { requestId: reqId });
178
178
  }
179
- 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
+ }
180
215
  });
181
216
  }
182
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/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.21-beta.0",
3
+ "version": "0.7.21-beta.1",
4
4
  "description": "The toolbox for cloudbase",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {