@cloudbase/toolbox 0.7.16-beta.2 → 0.7.17-beta.0

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,7 +1,9 @@
1
+ import { DeviceFlowOptions } from './oauth';
1
2
  import { Credential, RequestConfig } from '../types';
2
3
  export * from './common';
3
4
  export * from './credential';
4
5
  export * from './web-auth';
6
+ export * from './oauth';
5
7
  export interface AuthSupervisorOptions {
6
8
  /**
7
9
  * 是否在内存中缓存 credential 信息
@@ -20,15 +22,28 @@ export interface AuthSupervisorOptions {
20
22
  */
21
23
  throwError?: boolean;
22
24
  }
25
+ export type AuthFlowType = 'default' | 'web' | 'device';
23
26
  export interface WebAuthOptions {
24
27
  /**
25
28
  * 请在初始化实例时指定
26
29
  * @deprecated
27
30
  */
28
31
  throwError?: boolean;
32
+ /**
33
+ * 授权方式
34
+ * - 'default': 自动选择,优先 web,浏览器打开失败或无浏览器环境时降级到 device
35
+ * - 'web': 强制使用网页回调授权
36
+ * - 'device': 强制使用 Device Flow 授权
37
+ * @default 'default'
38
+ */
39
+ mode?: AuthFlowType;
29
40
  getAuthUrl?: (rawUrl: string) => string;
30
41
  noBrowser?: boolean;
31
42
  callbackTimeout?: number;
43
+ /** Device Flow 的客户端 ID */
44
+ client_id?: string;
45
+ /** Device Flow 回调,获取设备码后通知调用方 */
46
+ onDeviceCode?: DeviceFlowOptions['onDeviceCode'];
32
47
  }
33
48
  export interface LoginByApiSecretOptions {
34
49
  /**
package/lib/auth/index.js CHANGED
@@ -25,12 +25,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.AuthSupevisor = exports.AuthSupervisor = void 0;
27
27
  const web_auth_1 = require("./web-auth");
28
+ const oauth_1 = require("./oauth");
28
29
  const common_1 = require("./common");
29
30
  const credential_1 = require("./credential");
31
+ const web_1 = require("../web");
30
32
  const error_1 = require("../error");
31
33
  __exportStar(require("./common"), exports);
32
34
  __exportStar(require("./credential"), exports);
33
35
  __exportStar(require("./web-auth"), exports);
36
+ __exportStar(require("./oauth"), exports);
34
37
  class AuthSupervisor {
35
38
  /**
36
39
  * 单例模式,全局缓存
@@ -80,7 +83,7 @@ class AuthSupervisor {
80
83
  */
81
84
  loginByWebAuth(options = {}) {
82
85
  return __awaiter(this, void 0, void 0, function* () {
83
- const { getAuthUrl, throwError, noBrowser, callbackTimeout } = options;
86
+ const { getAuthUrl, throwError, noBrowser, callbackTimeout, mode: authFlowType, client_id, onDeviceCode } = options;
84
87
  if (this.cacheCredential && this.needCache && !this.isCacheExpire()) {
85
88
  return this.cacheCredential;
86
89
  }
@@ -88,12 +91,45 @@ class AuthSupervisor {
88
91
  let credential = yield (0, credential_1.checkAndGetCredential)(this.requestConfig);
89
92
  if (credential)
90
93
  return credential;
91
- // 兼容临时秘钥
92
- credential = yield (0, web_auth_1.getAuthTokenFromWeb)({
93
- getAuthUrl,
94
- noBrowser,
95
- callbackTimeout
96
- });
94
+ // 根据授权方式获取凭证
95
+ switch (authFlowType) {
96
+ case 'device':
97
+ credential = yield (0, oauth_1.getAuthTokenByDeviceFlow)({ client_id, onDeviceCode });
98
+ break;
99
+ case 'web':
100
+ credential = yield (0, web_auth_1.getAuthTokenFromWeb)({
101
+ getAuthUrl,
102
+ noBrowser,
103
+ callbackTimeout
104
+ });
105
+ break;
106
+ case 'default':
107
+ default: {
108
+ const isNoBrowser = noBrowser || (0, web_1.isTruthyFlag)(process.env.TCB_NO_BROWSER);
109
+ if (isNoBrowser) {
110
+ credential = yield (0, oauth_1.getAuthTokenByDeviceFlow)({ client_id, onDeviceCode });
111
+ }
112
+ else {
113
+ try {
114
+ credential = yield (0, web_auth_1.getAuthTokenFromWeb)({
115
+ getAuthUrl,
116
+ callbackTimeout,
117
+ onOpenFailed: () => { }
118
+ });
119
+ }
120
+ catch (e) {
121
+ if ((e === null || e === void 0 ? void 0 : e.code) === 'BROWSER_OPEN_FAILED') {
122
+ credential = yield (0, oauth_1.getAuthTokenByDeviceFlow)({ client_id, onDeviceCode });
123
+ }
124
+ else {
125
+ throw e;
126
+ }
127
+ }
128
+ }
129
+ break;
130
+ }
131
+ }
132
+ credential = (0, common_1.resolveCredential)(credential);
97
133
  try {
98
134
  yield (0, common_1.checkAuth)(credential, this.requestConfig);
99
135
  }
@@ -0,0 +1,11 @@
1
+ import { Credential } from "../types";
2
+ export interface DeviceFlowOptions {
3
+ client_id?: string;
4
+ onDeviceCode?: (data: {
5
+ user_code: string;
6
+ verification_uri?: string;
7
+ device_code: string;
8
+ expires_in: number;
9
+ }) => void;
10
+ }
11
+ export declare function getAuthTokenByDeviceFlow(options?: DeviceFlowOptions): Promise<Credential>;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getAuthTokenByDeviceFlow = void 0;
13
+ const net_1 = require("../net");
14
+ const web_1 = require("../web");
15
+ const error_1 = require("../error");
16
+ const coding_1 = require("../coding");
17
+ const system_1 = require("../system");
18
+ const web_auth_1 = require("./web-auth");
19
+ const OAUTH_ENDPOINT = process.env.TCB_OAUTH_ENDPOINT || 'https://tcb-api.cloud.tencent.com/qcloud-tcb/v1/oauth';
20
+ const DEFAULT_CLIENT_ID = 'cloudbase-toolbox';
21
+ const POLL_ERROR_CODES = {
22
+ AUTHORIZATION_PENDING: 'authorization_pending',
23
+ SLOW_DOWN: 'slow_down',
24
+ EXPIRED_TOKEN: 'expired_token',
25
+ ACCESS_DENIED: 'access_denied',
26
+ };
27
+ const SUCCESS_CODE = 'NORMAL';
28
+ function fetchDeviceCode(options = {}) {
29
+ const { client_id = DEFAULT_CLIENT_ID } = options;
30
+ return (0, net_1.postFetch)(`${OAUTH_ENDPOINT}/device/code`, { client_id });
31
+ }
32
+ function fetchPollToken(options) {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ const { device_code, client_id = DEFAULT_CLIENT_ID } = options;
35
+ const mac = yield (0, system_1.getMacAddress)();
36
+ return (0, net_1.postFetch)(`${OAUTH_ENDPOINT}/token`, {
37
+ device_code,
38
+ client_id,
39
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
40
+ device_info: {
41
+ mac,
42
+ os: (0, system_1.getOSInfo)(),
43
+ hash: (0, coding_1.md5Encoding)(mac),
44
+ }
45
+ });
46
+ });
47
+ }
48
+ function sleep(ms) {
49
+ return new Promise((resolve) => {
50
+ setTimeout(resolve, ms);
51
+ });
52
+ }
53
+ function getAuthTokenByDeviceFlow(options = {}) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ const { client_id, onDeviceCode } = options;
56
+ const deviceCodeResp = yield fetchDeviceCode({ client_id });
57
+ if (deviceCodeResp.code !== SUCCESS_CODE) {
58
+ throw new error_1.CloudBaseError('Device Flow fetchDeviceCode failed', {
59
+ code: deviceCodeResp.code,
60
+ requestId: deviceCodeResp.reqId,
61
+ });
62
+ }
63
+ const { device_code, user_code, expires_in, interval } = deviceCodeResp.result;
64
+ // const defaultVerificationUri = deviceCodeResp.result.verification_uri || `${CLI_AUTH_BASE_URL}#/cli-auth`;
65
+ // 暂时不消费 verification_uri,由客户端控制跳转
66
+ const defaultVerificationUri = `${web_auth_1.CLI_AUTH_BASE_URL}#/cli-auth`;
67
+ // from=cli 后续考虑改用 client_id,因为 toolbox 作为公共依赖,可能被不同 client 所引用
68
+ const verification_uri = `${defaultVerificationUri}?user_code=${user_code}&from=cli&mode=device`;
69
+ // 打印授权信息,引导用户操作
70
+ console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
71
+ console.log(`\n${verification_uri}`);
72
+ console.log(`\n用户码: ${user_code}\n`);
73
+ // 尝试自动打开授权页面
74
+ yield (0, web_1.openUrl)(verification_uri);
75
+ if (onDeviceCode) {
76
+ onDeviceCode({ user_code, verification_uri, device_code, expires_in });
77
+ }
78
+ let pollInterval = interval;
79
+ const deadline = Date.now() + expires_in * 1000;
80
+ while (Date.now() < deadline) {
81
+ yield sleep(pollInterval * 1000);
82
+ let resp;
83
+ try {
84
+ resp = yield fetchPollToken({ device_code, interval: pollInterval, client_id });
85
+ }
86
+ catch (e) {
87
+ throw new error_1.CloudBaseError('Device Flow 轮询请求失败', { original: e });
88
+ }
89
+ const { code, result } = resp;
90
+ // 服务端返回业务错误(result 中携带 error 字段)
91
+ if (result && 'error' in result) {
92
+ const { error, error_description } = result;
93
+ if (error === POLL_ERROR_CODES.AUTHORIZATION_PENDING) {
94
+ continue;
95
+ }
96
+ if (error === POLL_ERROR_CODES.SLOW_DOWN) {
97
+ pollInterval += 5;
98
+ continue;
99
+ }
100
+ if (error === POLL_ERROR_CODES.EXPIRED_TOKEN) {
101
+ throw new error_1.CloudBaseError('设备码已过期,请重新发起授权');
102
+ }
103
+ if (error === POLL_ERROR_CODES.ACCESS_DENIED) {
104
+ throw new error_1.CloudBaseError('用户拒绝了授权请求');
105
+ }
106
+ throw new error_1.CloudBaseError(error_description || `Device Flow 授权失败,错误: ${error}`, {
107
+ code: error,
108
+ requestId: resp.reqId,
109
+ });
110
+ }
111
+ // 服务端返回成功的 Credential
112
+ if (code === SUCCESS_CODE && result) {
113
+ return result;
114
+ }
115
+ throw new error_1.CloudBaseError(`Device Flow 授权失败,错误码: ${code}`, {
116
+ code,
117
+ requestId: resp.reqId,
118
+ });
119
+ }
120
+ throw new error_1.CloudBaseError('授权超时,用户未在有效期内完成授权,请重试');
121
+ });
122
+ }
123
+ exports.getAuthTokenByDeviceFlow = getAuthTokenByDeviceFlow;
@@ -1,6 +1,8 @@
1
1
  import { Credential } from '../types';
2
+ export declare const CLI_AUTH_BASE_URL = "https://tcb.cloud.tencent.com/dev";
2
3
  export declare function getAuthTokenFromWeb(options?: {
3
4
  getAuthUrl?: (rawUrl: string) => string;
4
5
  noBrowser?: boolean;
5
6
  callbackTimeout?: number;
7
+ onOpenFailed?: () => void;
6
8
  }): Promise<Credential>;
@@ -9,16 +9,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getAuthTokenFromWeb = void 0;
12
+ exports.getAuthTokenFromWeb = exports.CLI_AUTH_BASE_URL = void 0;
13
13
  const common_1 = require("./common");
14
14
  const coding_1 = require("../coding");
15
15
  const web_1 = require("../web");
16
16
  const system_1 = require("../system");
17
- const CliAuthBaseUrl = 'https://tcb.cloud.tencent.com/dev';
17
+ 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 } = options;
21
+ const { getAuthUrl, noBrowser, callbackTimeout, onOpenFailed } = 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);
@@ -31,7 +31,7 @@ function getAuthTokenFromWeb(options = {}) {
31
31
  + '&from=cli';
32
32
  const encodedCallbackUrl = encodeURIComponent(callbackUrl);
33
33
  // 授权链接
34
- const rawAuthUrl = `${CliAuthBaseUrl}?authCallbackUrl=${encodedCallbackUrl}#/cli-auth?${encodedQuery}`;
34
+ const rawAuthUrl = `${exports.CLI_AUTH_BASE_URL}?authCallbackUrl=${encodedCallbackUrl}#/cli-auth?${encodedQuery}`;
35
35
  let cliAuthUrl = rawAuthUrl;
36
36
  if (getAuthUrl) {
37
37
  try {
@@ -44,7 +44,8 @@ function getAuthTokenFromWeb(options = {}) {
44
44
  return cliAuthUrl;
45
45
  }, 'login', {
46
46
  noBrowser,
47
- callbackTimeout
47
+ callbackTimeout,
48
+ onOpenFailed
48
49
  });
49
50
  const credential = (0, common_1.resolveCredential)(query);
50
51
  return credential;
package/lib/web/web.d.ts CHANGED
@@ -6,5 +6,11 @@ export type CheckFn = (query: IQuery) => Promise<void>;
6
6
  export interface GetDataFromWebOptions {
7
7
  noBrowser?: boolean;
8
8
  callbackTimeout?: number;
9
+ /** 浏览器打开失败时的回调,若提供则中止等待并抛出错误 */
10
+ onOpenFailed?: () => void;
9
11
  }
12
+ export declare function isTruthyFlag(value?: string): boolean;
13
+ export declare function openUrl(url: string, options?: {
14
+ skipBrowserFallback?: boolean;
15
+ }): Promise<boolean>;
10
16
  export declare function getDataFromWeb<T extends IQuery>(getUrl: GetUrlFn, type: 'login' | 'getData', options?: GetDataFromWebOptions): Promise<T>;
package/lib/web/web.js CHANGED
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.getDataFromWeb = void 0;
15
+ exports.getDataFromWeb = exports.openUrl = exports.isTruthyFlag = void 0;
16
16
  const open_1 = __importDefault(require("open"));
17
17
  const query_string_1 = __importDefault(require("query-string"));
18
18
  const http_1 = __importDefault(require("http"));
@@ -59,6 +59,7 @@ function isTruthyFlag(value) {
59
59
  }
60
60
  return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase());
61
61
  }
62
+ exports.isTruthyFlag = isTruthyFlag;
62
63
  function isVSCodeEnvironment() {
63
64
  return Boolean(process.env.VSCODE_IPC_HOOK_CLI
64
65
  || process.env.VSCODE_PID
@@ -93,14 +94,13 @@ function openUrlByBrowserEnv(url) {
93
94
  function shouldUseBrowserEnvFallback() {
94
95
  return process.platform === 'linux' && isVSCodeEnvironment();
95
96
  }
96
- function openUrl(url) {
97
+ function openUrl(url, options = {}) {
97
98
  return __awaiter(this, void 0, void 0, function* () {
99
+ const { skipBrowserFallback = false } = options;
98
100
  try {
99
101
  const child = yield (0, open_1.default)(url, { url: true });
100
- if (child === null || child === void 0 ? void 0 : child.once) {
102
+ if ((child === null || child === void 0 ? void 0 : child.once) && !skipBrowserFallback) {
101
103
  child.once('error', (error) => __awaiter(this, void 0, void 0, function* () {
102
- const code = (error === null || error === void 0 ? void 0 : error.code) || 'UNKNOWN';
103
- console.warn(`自动打开浏览器失败(${code})。`);
104
104
  if (shouldUseBrowserEnvFallback()) {
105
105
  yield openUrlByBrowserEnv(url);
106
106
  }
@@ -109,13 +109,14 @@ function openUrl(url) {
109
109
  return true;
110
110
  }
111
111
  catch (e) {
112
- if (shouldUseBrowserEnvFallback()) {
112
+ if (!skipBrowserFallback && shouldUseBrowserEnvFallback()) {
113
113
  return openUrlByBrowserEnv(url);
114
114
  }
115
115
  return false;
116
116
  }
117
117
  });
118
118
  }
119
+ exports.openUrl = openUrl;
119
120
  // 从 Web 页面中获取数据
120
121
  function getDataFromWeb(getUrl, type, options = {}) {
121
122
  var _a, _b;
@@ -127,13 +128,19 @@ function getDataFromWeb(getUrl, type, options = {}) {
127
128
  throw new error_1.CloudBaseError('callbackTimeout must be a positive number');
128
129
  }
129
130
  const url = getUrl(port);
130
- console.log('\n\n若链接未自动打开,请手动复制此链接至浏览器,或尝试使用其他登录方式:');
131
- console.log(`\n${url}`);
131
+ console.log('\n\n若链接未自动打开,请手动复制至浏览器,或尝试其他登录方式:');
132
+ console.log(`\n${url}\n`);
132
133
  if (!noBrowser) {
133
134
  // 对 url 转码, 避免 wsl 无法正常打开地址
134
135
  // https://www.npmjs.com/package/open#url
135
136
  // https://github.com/sindresorhus/open/blob/master/index.js#L48
136
- yield openUrl(url);
137
+ const skipBrowserFallback = !!options.onOpenFailed;
138
+ const opened = yield openUrl(url, { skipBrowserFallback });
139
+ if (!opened && options.onOpenFailed) {
140
+ server.close();
141
+ options.onOpenFailed();
142
+ throw new error_1.CloudBaseError('浏览器打开失败', { code: 'BROWSER_OPEN_FAILED' });
143
+ }
137
144
  }
138
145
  return new Promise((resolve, reject) => {
139
146
  let finished = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/toolbox",
3
- "version": "0.7.16-beta.2",
3
+ "version": "0.7.17-beta.0",
4
4
  "description": "The toolbox for cloudbase",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -67,4 +67,4 @@
67
67
  }
68
68
  },
69
69
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
70
- }
70
+ }