@cloudbase/toolbox 0.7.16 → 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,11 +94,12 @@ 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
104
  if (shouldUseBrowserEnvFallback()) {
103
105
  yield openUrlByBrowserEnv(url);
@@ -107,13 +109,14 @@ function openUrl(url) {
107
109
  return true;
108
110
  }
109
111
  catch (e) {
110
- if (shouldUseBrowserEnvFallback()) {
112
+ if (!skipBrowserFallback && shouldUseBrowserEnvFallback()) {
111
113
  return openUrlByBrowserEnv(url);
112
114
  }
113
115
  return false;
114
116
  }
115
117
  });
116
118
  }
119
+ exports.openUrl = openUrl;
117
120
  // 从 Web 页面中获取数据
118
121
  function getDataFromWeb(getUrl, type, options = {}) {
119
122
  var _a, _b;
@@ -131,7 +134,13 @@ function getDataFromWeb(getUrl, type, options = {}) {
131
134
  // 对 url 转码, 避免 wsl 无法正常打开地址
132
135
  // https://www.npmjs.com/package/open#url
133
136
  // https://github.com/sindresorhus/open/blob/master/index.js#L48
134
- 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
+ }
135
144
  }
136
145
  return new Promise((resolve, reject) => {
137
146
  let finished = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/toolbox",
3
- "version": "0.7.16",
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
+ }