tyc-cli 0.3.6 → 0.3.8
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.
- package/README.md +11 -10
- package/dist/commands/init.js +36 -10
- package/dist/commands/login.js +134 -33
- package/dist/index.js +3 -2
- package/dist/oauth.js +98 -0
- package/dist/oauthPending.js +3 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,9 +61,9 @@ tyc login
|
|
|
61
61
|
tyc login --url "https://ai-mcp-pre.tianyancha.com/mcp"
|
|
62
62
|
tyc login --url "http://localhost:8080/mcp" --issuer "http://localhost:8080/oauth"
|
|
63
63
|
|
|
64
|
-
# 非阻塞 OAuth
|
|
65
|
-
tyc login --no-
|
|
66
|
-
tyc login --
|
|
64
|
+
# 非阻塞 OAuth:打印授权 URL 和 6 位验证码;网页授权后恢复登录
|
|
65
|
+
tyc login --no-block
|
|
66
|
+
tyc login --resume
|
|
67
67
|
|
|
68
68
|
# API key 兼容路径:已有天眼查 OpenAPI token 时可直接写入
|
|
69
69
|
tyc init --authorization "YOUR_API_TOKEN"
|
|
@@ -77,11 +77,11 @@ tyc init --authorization "YOUR_API_TOKEN" --no-verify
|
|
|
77
77
|
> `Authorization: Bearer <access_token>` 和 refresh token 上下文写入
|
|
78
78
|
> `~/.tyc/config.json`。后续业务命令会在 access token 临期时自动刷新;
|
|
79
79
|
> 如果服务端返回 401,会强制刷新一次并重试当前请求。
|
|
80
|
-
> `tyc login --no-
|
|
81
|
-
> 保存到 `~/.tyc/oauth_pending.json
|
|
82
|
-
>
|
|
83
|
-
> `tyc
|
|
84
|
-
>
|
|
80
|
+
> `tyc login --no-block` 使用 OAuth Device Flow,打印授权 URL 和 6 位验证码后立即退出,
|
|
81
|
+
> 并把一次性 `device_code` 保存到 `~/.tyc/oauth_pending.json`。在网页输入验证码并确认授权后,
|
|
82
|
+
> 执行 `tyc login --resume` 完成换 token。旧版 `tyc login --callback-token "<...>"` 仍保留作兼容入口。
|
|
83
|
+
> `tyc init --authorization` 保存配置后会立即校验 shared core auth ready;通过后输出与
|
|
84
|
+
> OAuth 登录一致的登录成功提示。失败则退出码 1 并提示配置校验问题。
|
|
85
85
|
> 加 `--no-verify` 可跳过校验。
|
|
86
86
|
|
|
87
87
|
配置存于 `~/.tyc/config.json`(权限 600):
|
|
@@ -123,8 +123,9 @@ tyc executive personnel-dishonest "..." --humanName "张三"
|
|
|
123
123
|
| 命令 | 说明 |
|
|
124
124
|
|------|------|
|
|
125
125
|
| `tyc login` | 浏览器 OAuth 登录;自动发现 metadata、使用 PKCE 获取 access token / refresh token 并写入配置 |
|
|
126
|
-
| `tyc login --no-
|
|
127
|
-
| `tyc login --
|
|
126
|
+
| `tyc login --no-block` | 打印 Device Flow 授权 URL 和 6 位验证码并立即退出 |
|
|
127
|
+
| `tyc login --resume` | 网页输入验证码并确认授权后,使用本地 pending device_code 换取 access token |
|
|
128
|
+
| `tyc login --callback-token <code-or-url>` | 兼容旧版 PKCE 非阻塞登录,把回调 code 换成 access token |
|
|
128
129
|
| `tyc login --url <url>` | 对指定 MCP endpoint 发起 OAuth 登录 |
|
|
129
130
|
| `tyc init --authorization <token>` | 写入 `headers.Authorization`;保存后会立即校验 shared core 连通性 |
|
|
130
131
|
| `tyc init --url <url>` | 设置 MCP endpoint |
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig,
|
|
2
|
-
import { verifyCoreEndpoint } from "../coreClient.js";
|
|
1
|
+
import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, saveConfig, } from "../config.js";
|
|
2
|
+
import { verifyCoreAuthEndpoint, verifyCoreEndpoint } from "../coreClient.js";
|
|
3
|
+
import { printLoginSuccessBanner } from "../logo.js";
|
|
3
4
|
export function registerInitCommand(program) {
|
|
4
5
|
program
|
|
5
6
|
.command("init")
|
|
6
|
-
.description("配置 endpoint / Authorization(保存到 ~/.tyc/config.json
|
|
7
|
+
.description("配置 endpoint / Authorization(保存到 ~/.tyc/config.json,并校验配置)")
|
|
7
8
|
.option("--authorization <token>", "tyc OpenAPI Authorization(写入 headers.Authorization)")
|
|
8
9
|
.option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL})`)
|
|
9
10
|
.option("--core-url <url>", "Shared Core HTTP endpoint(默认从 --url 推导 /v1/core/tools/call)")
|
|
@@ -52,21 +53,46 @@ export function registerInitCommand(program) {
|
|
|
52
53
|
}
|
|
53
54
|
if (resetOAuth)
|
|
54
55
|
delete cfg.oauth;
|
|
56
|
+
const persistedAuthorization = cfg.headers.Authorization?.trim();
|
|
55
57
|
saveConfig(cfg);
|
|
56
|
-
|
|
58
|
+
const verbose = !!program.opts().verbose;
|
|
59
|
+
if (verbose) {
|
|
60
|
+
console.error(`> config saved to ~/.tyc/config.json (url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
|
|
61
|
+
}
|
|
57
62
|
if (opts.verify === false) {
|
|
63
|
+
console.log(`已保存 ~/.tyc/config.json (url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
|
|
58
64
|
return;
|
|
59
65
|
}
|
|
60
66
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
const verifyConfig = {
|
|
68
|
+
url: cfg.url,
|
|
69
|
+
coreUrl: cfg.coreUrl || defaultCoreURL(cfg.url),
|
|
70
|
+
headers: { ...(cfg.headers || {}) },
|
|
71
|
+
};
|
|
72
|
+
if (!verifyConfig.headers.Authorization && process.env.TYC_AUTHORIZATION) {
|
|
73
|
+
verifyConfig.headers.Authorization = process.env.TYC_AUTHORIZATION;
|
|
74
|
+
}
|
|
75
|
+
if (verifyConfig.headers.Authorization?.trim()) {
|
|
76
|
+
await verifyCoreAuthEndpoint(verifyConfig, verbose);
|
|
77
|
+
if (verbose) {
|
|
78
|
+
console.error("> Shared Core auth check passed");
|
|
79
|
+
}
|
|
80
|
+
if (persistedAuthorization) {
|
|
81
|
+
printLoginSuccessBanner();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log("Authorization 校验通过(来自 TYC_AUTHORIZATION;未写入 ~/.tyc/config.json)");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await verifyCoreEndpoint(verifyConfig, verbose);
|
|
89
|
+
console.log("Shared Core endpoint 校验通过");
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
catch (err) {
|
|
67
93
|
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
-
console.error(
|
|
69
|
-
console.error("配置已保存;确认 --url
|
|
94
|
+
console.error(`配置校验失败:${msg}`);
|
|
95
|
+
console.error("配置已保存;确认 --url 可达且 Authorization 有效后重新运行 tyc init,或加 --no-verify 跳过校验。");
|
|
70
96
|
process.exit(1);
|
|
71
97
|
}
|
|
72
98
|
});
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MCP_URL, applyOAuthTokenToConfig, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
|
|
2
2
|
import { verifyCoreAuthEndpoint } from "../coreClient.js";
|
|
3
|
-
import { buildAuthorizationURL, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, parseAuthorizationCallbackCode, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
|
|
3
|
+
import { buildAuthorizationURL, buildCliCallbackRedirectUri, createPKCE, DEVICE_CODE_GRANT_TYPE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, exchangeDeviceCode, knownOAuthDefaultsForMcpURL, OAuthTokenEndpointError, openBrowser, parseAuthorizationCallbackCode, randomState, registerDynamicOAuthClient, requestDeviceAuthorization, startCallbackServer, } from "../oauth.js";
|
|
4
4
|
import { clearPendingOAuthLogin, loadPendingOAuthLogin, savePendingOAuthLogin, } from "../oauthPending.js";
|
|
5
5
|
import { printLoginSuccessBanner } from "../logo.js";
|
|
6
6
|
function parsePort(raw) {
|
|
@@ -16,12 +16,6 @@ function normalizeCallbackPath(raw) {
|
|
|
16
16
|
const path = raw || "/oauth/callback";
|
|
17
17
|
return path.startsWith("/") ? path : `/${path}`;
|
|
18
18
|
}
|
|
19
|
-
function buildLoopbackRedirectUri(host, port, path) {
|
|
20
|
-
if (port === 0) {
|
|
21
|
-
throw new Error("--no-block 不能与 --redirect-port 0 同时使用,请指定固定回调端口。");
|
|
22
|
-
}
|
|
23
|
-
return `http://${host}:${port}${path}`;
|
|
24
|
-
}
|
|
25
19
|
function pickCallbackToken(opts) {
|
|
26
20
|
if (opts.callbackToken && opts.token) {
|
|
27
21
|
throw new Error("--callback-token 和 --token 只能指定一个。");
|
|
@@ -51,6 +45,15 @@ async function verifySavedOAuthLogin(verify, verbose) {
|
|
|
51
45
|
}
|
|
52
46
|
printLoginSuccessBanner();
|
|
53
47
|
}
|
|
48
|
+
function isPendingDeviceLogin(pending) {
|
|
49
|
+
return pending.version === 2;
|
|
50
|
+
}
|
|
51
|
+
function isPendingPKCELogin(pending) {
|
|
52
|
+
return pending.version === 1;
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
54
57
|
async function completePendingOAuthLogin(pending, callbackToken, verify, verbose) {
|
|
55
58
|
const code = parseAuthorizationCallbackCode(callbackToken, pending.state);
|
|
56
59
|
if (verbose) {
|
|
@@ -74,6 +77,63 @@ async function completePendingOAuthLogin(pending, callbackToken, verify, verbose
|
|
|
74
77
|
clearPendingOAuthLogin(verbose);
|
|
75
78
|
await verifySavedOAuthLogin(verify, verbose);
|
|
76
79
|
}
|
|
80
|
+
async function completePendingDeviceLogin(pending, verify, verbose) {
|
|
81
|
+
if (pending.expiresAt <= Date.now()) {
|
|
82
|
+
clearPendingOAuthLogin(verbose);
|
|
83
|
+
throw new Error("OAuth device 授权已过期,请重新运行 tyc login --no-block。");
|
|
84
|
+
}
|
|
85
|
+
let intervalMs = Math.max(1, pending.interval || 5) * 1000;
|
|
86
|
+
const deadline = Date.now() + 30_000;
|
|
87
|
+
for (;;) {
|
|
88
|
+
try {
|
|
89
|
+
if (verbose) {
|
|
90
|
+
console.error("> polling OAuth device token endpoint");
|
|
91
|
+
}
|
|
92
|
+
const token = await exchangeDeviceCode({
|
|
93
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
94
|
+
clientId: pending.clientId,
|
|
95
|
+
clientSecret: pending.clientSecret,
|
|
96
|
+
deviceCode: pending.deviceCode,
|
|
97
|
+
resource: pending.resource,
|
|
98
|
+
});
|
|
99
|
+
saveOAuthTokenConfig(pending.mcpURL, token, {
|
|
100
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
101
|
+
clientId: pending.clientId,
|
|
102
|
+
clientSecret: pending.clientSecret,
|
|
103
|
+
resource: pending.resource,
|
|
104
|
+
scope: pending.scope,
|
|
105
|
+
}, verbose);
|
|
106
|
+
clearPendingOAuthLogin(verbose);
|
|
107
|
+
await verifySavedOAuthLogin(verify, verbose);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!(err instanceof OAuthTokenEndpointError)) {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
if (err.error === "authorization_pending" || err.error === "slow_down") {
|
|
115
|
+
if (err.error === "slow_down") {
|
|
116
|
+
intervalMs += 5_000;
|
|
117
|
+
}
|
|
118
|
+
if (Date.now() + intervalMs > deadline) {
|
|
119
|
+
throw new Error("授权尚未完成。请在网页输入 6 位验证码并确认后,再运行 tyc login --resume。");
|
|
120
|
+
}
|
|
121
|
+
await sleep(intervalMs);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (["expired_token", "access_denied", "invalid_grant"].includes(err.error)) {
|
|
125
|
+
clearPendingOAuthLogin(verbose);
|
|
126
|
+
}
|
|
127
|
+
if (err.error === "expired_token") {
|
|
128
|
+
throw new Error("OAuth device 授权已过期,请重新运行 tyc login --no-block。");
|
|
129
|
+
}
|
|
130
|
+
if (err.error === "access_denied") {
|
|
131
|
+
throw new Error("OAuth device 授权已被拒绝,请重新运行 tyc login --no-block。");
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
77
137
|
export function registerLoginCommand(program) {
|
|
78
138
|
program
|
|
79
139
|
.command("login")
|
|
@@ -87,21 +147,40 @@ export function registerLoginCommand(program) {
|
|
|
87
147
|
.option("--resource-metadata-url <url>", "手动指定 MCP protected resource metadata URL")
|
|
88
148
|
.option("--authorization-endpoint <url>", "手动指定 OAuth authorization endpoint")
|
|
89
149
|
.option("--token-endpoint <url>", "手动指定 OAuth token endpoint")
|
|
150
|
+
.option("--device-authorization-endpoint <url>", "手动指定 OAuth device authorization endpoint")
|
|
90
151
|
.option("--redirect-host <host>", "本地回调 host(默认 localhost)")
|
|
91
152
|
.option("--redirect-port <port>", "本地回调端口(默认 7078)")
|
|
92
153
|
.option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
|
|
93
154
|
.option("--callback-token <code-or-url>", "继续非阻塞登录:传入授权回调 code 或包含 code 的完整 callback URL")
|
|
94
155
|
.option("--token <code-or-url>", "同 --callback-token(兼容别名;仍需传授权回调 code)")
|
|
156
|
+
.option("--resume", "继续 RFC 8628 device flow 非阻塞登录")
|
|
95
157
|
.option("--no-open", "不自动打开浏览器,只打印授权 URL")
|
|
96
|
-
.option("--no-block", "
|
|
158
|
+
.option("--no-block", "打印 Device Flow 授权 URL 和 6 位验证码并保存 pending 状态后立即退出")
|
|
97
159
|
.option("--no-verify", "保存 token 后跳过 shared core 鉴权校验")
|
|
98
160
|
.action(async (opts) => {
|
|
99
161
|
const verbose = !!program.opts().verbose;
|
|
100
162
|
const callbackToken = pickCallbackToken(opts);
|
|
163
|
+
if (opts.resume) {
|
|
164
|
+
if (callbackToken) {
|
|
165
|
+
throw new Error("--resume 不能与 --callback-token/--token 同时使用。");
|
|
166
|
+
}
|
|
167
|
+
const pending = loadPendingOAuthLogin();
|
|
168
|
+
if (!pending) {
|
|
169
|
+
throw new Error("未找到待完成的 OAuth device 登录状态。请先运行 tyc login --no-block。");
|
|
170
|
+
}
|
|
171
|
+
if (!isPendingDeviceLogin(pending)) {
|
|
172
|
+
throw new Error("当前待完成的是旧版 callback 登录,请使用 tyc login --callback-token <code-or-url>。");
|
|
173
|
+
}
|
|
174
|
+
await completePendingDeviceLogin(pending, opts.verify !== false && pending.verify !== false, verbose);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
101
177
|
if (callbackToken) {
|
|
102
178
|
const pending = loadPendingOAuthLogin();
|
|
103
179
|
if (!pending) {
|
|
104
|
-
throw new Error("未找到待完成的 OAuth 登录状态。请先运行 tyc login --no-
|
|
180
|
+
throw new Error("未找到待完成的 OAuth 登录状态。请先运行 tyc login --no-block。");
|
|
181
|
+
}
|
|
182
|
+
if (!isPendingPKCELogin(pending)) {
|
|
183
|
+
throw new Error("当前待完成的是 device 登录,请在网页确认后运行 tyc login --resume。");
|
|
105
184
|
}
|
|
106
185
|
if (verbose) {
|
|
107
186
|
console.error(`> loaded pending OAuth login from ~/.tyc/oauth_pending.json (url=${pending.mcpURL})`);
|
|
@@ -110,9 +189,6 @@ export function registerLoginCommand(program) {
|
|
|
110
189
|
return;
|
|
111
190
|
}
|
|
112
191
|
const noBlock = opts.block === false;
|
|
113
|
-
if (noBlock && opts.open !== false) {
|
|
114
|
-
throw new Error("--no-block 需要与 --no-open 一起使用。");
|
|
115
|
-
}
|
|
116
192
|
const current = loadConfig() || {};
|
|
117
193
|
const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
|
|
118
194
|
const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
|
|
@@ -164,6 +240,7 @@ export function registerLoginCommand(program) {
|
|
|
164
240
|
endpoints = {
|
|
165
241
|
authorizationEndpoint: opts.authorizationEndpoint,
|
|
166
242
|
tokenEndpoint: opts.tokenEndpoint,
|
|
243
|
+
deviceAuthorizationEndpoint: opts.deviceAuthorizationEndpoint,
|
|
167
244
|
issuer,
|
|
168
245
|
};
|
|
169
246
|
}
|
|
@@ -172,11 +249,16 @@ export function registerLoginCommand(program) {
|
|
|
172
249
|
endpoints.authorizationEndpoint =
|
|
173
250
|
opts.authorizationEndpoint || endpoints.authorizationEndpoint;
|
|
174
251
|
endpoints.tokenEndpoint = opts.tokenEndpoint || endpoints.tokenEndpoint;
|
|
252
|
+
endpoints.deviceAuthorizationEndpoint =
|
|
253
|
+
opts.deviceAuthorizationEndpoint || endpoints.deviceAuthorizationEndpoint;
|
|
175
254
|
}
|
|
176
255
|
resource ||= endpoints.resourceDocumentation || mcpURL;
|
|
177
256
|
if (verbose) {
|
|
178
257
|
console.error(`> authorization endpoint: ${endpoints.authorizationEndpoint}`);
|
|
179
258
|
console.error(`> token endpoint: ${endpoints.tokenEndpoint}`);
|
|
259
|
+
if (endpoints.deviceAuthorizationEndpoint) {
|
|
260
|
+
console.error(`> device authorization endpoint: ${endpoints.deviceAuthorizationEndpoint}`);
|
|
261
|
+
}
|
|
180
262
|
if (endpoints.registrationEndpoint) {
|
|
181
263
|
console.error(`> registration endpoint: ${endpoints.registrationEndpoint}`);
|
|
182
264
|
}
|
|
@@ -185,7 +267,7 @@ export function registerLoginCommand(program) {
|
|
|
185
267
|
const pkce = createPKCE();
|
|
186
268
|
let callback;
|
|
187
269
|
const redirectUri = noBlock
|
|
188
|
-
?
|
|
270
|
+
? buildCliCallbackRedirectUri(issuer)
|
|
189
271
|
: (callback = await startCallbackServer({
|
|
190
272
|
host: redirectHost,
|
|
191
273
|
port: redirectPort,
|
|
@@ -207,6 +289,10 @@ export function registerLoginCommand(program) {
|
|
|
207
289
|
registrationEndpoint: endpoints.registrationEndpoint,
|
|
208
290
|
clientName: "tyc-cli",
|
|
209
291
|
redirectUris: [redirectUri],
|
|
292
|
+
grantTypes: noBlock
|
|
293
|
+
? ["authorization_code", "refresh_token", DEVICE_CODE_GRANT_TYPE]
|
|
294
|
+
: undefined,
|
|
295
|
+
responseTypes: ["code"],
|
|
210
296
|
tokenEndpointAuthMethod: "none",
|
|
211
297
|
});
|
|
212
298
|
}
|
|
@@ -220,40 +306,55 @@ export function registerLoginCommand(program) {
|
|
|
220
306
|
console.error(`> registered client_id: ${clientId}`);
|
|
221
307
|
}
|
|
222
308
|
}
|
|
223
|
-
const authorizationURL = buildAuthorizationURL({
|
|
224
|
-
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
225
|
-
clientId,
|
|
226
|
-
redirectUri,
|
|
227
|
-
scope,
|
|
228
|
-
state,
|
|
229
|
-
codeChallenge: pkce.codeChallenge,
|
|
230
|
-
resource,
|
|
231
|
-
});
|
|
232
|
-
if (verbose) {
|
|
233
|
-
console.error(`> OAuth callback: ${redirectUri}`);
|
|
234
|
-
}
|
|
235
309
|
if (noBlock) {
|
|
310
|
+
if (!endpoints.deviceAuthorizationEndpoint) {
|
|
311
|
+
throw new Error("OAuth metadata 未提供 device_authorization_endpoint,请升级授权服务或使用 --device-authorization-endpoint 指定。");
|
|
312
|
+
}
|
|
313
|
+
const device = await requestDeviceAuthorization({
|
|
314
|
+
deviceAuthorizationEndpoint: endpoints.deviceAuthorizationEndpoint,
|
|
315
|
+
clientId,
|
|
316
|
+
clientSecret,
|
|
317
|
+
scope,
|
|
318
|
+
resource,
|
|
319
|
+
});
|
|
236
320
|
savePendingOAuthLogin({
|
|
237
|
-
version:
|
|
321
|
+
version: 2,
|
|
322
|
+
flow: "device",
|
|
238
323
|
createdAt: Date.now(),
|
|
239
324
|
mcpURL,
|
|
240
325
|
tokenEndpoint: endpoints.tokenEndpoint,
|
|
241
326
|
clientId,
|
|
242
327
|
clientSecret,
|
|
243
|
-
|
|
244
|
-
|
|
328
|
+
deviceCode: device.device_code,
|
|
329
|
+
userCode: device.user_code,
|
|
330
|
+
verificationUri: device.verification_uri,
|
|
331
|
+
expiresAt: Date.now() + device.expires_in * 1000,
|
|
332
|
+
interval: device.interval || 5,
|
|
245
333
|
resource,
|
|
246
|
-
|
|
334
|
+
scope,
|
|
247
335
|
verify: opts.verify !== false,
|
|
248
336
|
});
|
|
249
337
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
250
|
-
console.error(
|
|
338
|
+
console.error(device.verification_uri);
|
|
339
|
+
console.error("");
|
|
340
|
+
console.error(`验证码:${device.user_code}`);
|
|
251
341
|
console.error("");
|
|
252
|
-
console.error("
|
|
253
|
-
console.error("tyc login --
|
|
254
|
-
console.error("也可以传完整 callback URL:tyc login --callback-token \"http://localhost:7078/oauth/callback?code=...&state=...\"");
|
|
342
|
+
console.error("在网页输入 6 位验证码并确认授权后,运行:");
|
|
343
|
+
console.error("tyc login --resume");
|
|
255
344
|
return;
|
|
256
345
|
}
|
|
346
|
+
const authorizationURL = buildAuthorizationURL({
|
|
347
|
+
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
348
|
+
clientId,
|
|
349
|
+
redirectUri,
|
|
350
|
+
scope,
|
|
351
|
+
state,
|
|
352
|
+
codeChallenge: pkce.codeChallenge,
|
|
353
|
+
resource,
|
|
354
|
+
});
|
|
355
|
+
if (verbose) {
|
|
356
|
+
console.error(`> OAuth callback: ${redirectUri}`);
|
|
357
|
+
}
|
|
257
358
|
if (opts.open === false) {
|
|
258
359
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
259
360
|
console.error(authorizationURL);
|
package/dist/index.js
CHANGED
|
@@ -67,8 +67,9 @@ ${renderCategoryLines()}
|
|
|
67
67
|
初始化与登录
|
|
68
68
|
|
|
69
69
|
tyc login OAuth 浏览器登录
|
|
70
|
-
tyc login --no-
|
|
71
|
-
tyc login --
|
|
70
|
+
tyc login --no-block 打印 Device Flow URL 和 6 位验证码后立即退出
|
|
71
|
+
tyc login --resume 继续非阻塞 Device Flow 登录
|
|
72
|
+
tyc login --callback-token <CODE_OR_CALLBACK_URL> 继续旧版 callback OAuth 登录
|
|
72
73
|
tyc login --url <MCP_URL> 登录本地、预发或自托管 MCP
|
|
73
74
|
tyc init --authorization <KEY> 使用 API Key 兼容路径
|
|
74
75
|
tyc init --url <MCP_URL> --authorization <KEY> 配置本地或自托管服务
|
package/dist/oauth.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
+
export const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
5
|
+
export class OAuthTokenEndpointError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
error;
|
|
8
|
+
errorDescription;
|
|
9
|
+
constructor(status, error, errorDescription) {
|
|
10
|
+
super(errorDescription ? `${error}: ${errorDescription}` : error);
|
|
11
|
+
this.name = "OAuthTokenEndpointError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.error = error;
|
|
14
|
+
this.errorDescription = errorDescription;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
function trimRightSlash(v) {
|
|
5
18
|
return v.replace(/\/+$/, "");
|
|
6
19
|
}
|
|
@@ -123,10 +136,16 @@ export async function discoverAuthorizationServer(issuer) {
|
|
|
123
136
|
return {
|
|
124
137
|
authorizationEndpoint: metadata.authorization_endpoint,
|
|
125
138
|
tokenEndpoint: metadata.token_endpoint,
|
|
139
|
+
deviceAuthorizationEndpoint: typeof metadata.device_authorization_endpoint === "string"
|
|
140
|
+
? metadata.device_authorization_endpoint
|
|
141
|
+
: undefined,
|
|
126
142
|
registrationEndpoint: typeof metadata.registration_endpoint === "string"
|
|
127
143
|
? metadata.registration_endpoint
|
|
128
144
|
: undefined,
|
|
129
145
|
issuer,
|
|
146
|
+
grantTypesSupported: Array.isArray(metadata.grant_types_supported)
|
|
147
|
+
? metadata.grant_types_supported.filter((v) => typeof v === "string")
|
|
148
|
+
: undefined,
|
|
130
149
|
resourceDocumentation: typeof metadata.resource_documentation === "string"
|
|
131
150
|
? metadata.resource_documentation
|
|
132
151
|
: undefined,
|
|
@@ -172,6 +191,10 @@ export function createPKCE() {
|
|
|
172
191
|
export function randomState() {
|
|
173
192
|
return randomBytes(24).toString("base64url");
|
|
174
193
|
}
|
|
194
|
+
export function buildCliCallbackRedirectUri(issuer) {
|
|
195
|
+
const url = new URL(issuer);
|
|
196
|
+
return `${trimRightSlash(url.toString())}/cli/callback`;
|
|
197
|
+
}
|
|
175
198
|
function callbackHTML(title, body) {
|
|
176
199
|
return `<!doctype html>
|
|
177
200
|
<meta charset="utf-8">
|
|
@@ -311,6 +334,54 @@ export async function registerDynamicOAuthClient(opts) {
|
|
|
311
334
|
clientSecret: registered.client_secret,
|
|
312
335
|
};
|
|
313
336
|
}
|
|
337
|
+
export async function requestDeviceAuthorization(opts) {
|
|
338
|
+
const body = new URLSearchParams();
|
|
339
|
+
body.set("client_id", opts.clientId);
|
|
340
|
+
body.set("scope", opts.scope);
|
|
341
|
+
if (opts.clientSecret)
|
|
342
|
+
body.set("client_secret", opts.clientSecret);
|
|
343
|
+
if (opts.resource)
|
|
344
|
+
body.set("resource", opts.resource);
|
|
345
|
+
const resp = await fetch(opts.deviceAuthorizationEndpoint, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
Accept: "application/json",
|
|
349
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
350
|
+
},
|
|
351
|
+
body,
|
|
352
|
+
});
|
|
353
|
+
const text = await resp.text();
|
|
354
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
355
|
+
throw new Error(`device authorization HTTP ${resp.status}: ${text.slice(0, 500)}`);
|
|
356
|
+
}
|
|
357
|
+
const payload = JSON.parse(text);
|
|
358
|
+
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
359
|
+
throw new Error("device authorization response missing required fields");
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
...payload,
|
|
363
|
+
device_code: payload.device_code,
|
|
364
|
+
user_code: payload.user_code,
|
|
365
|
+
verification_uri: payload.verification_uri,
|
|
366
|
+
expires_in: Number(payload.expires_in) > 0 ? Number(payload.expires_in) : 600,
|
|
367
|
+
interval: Number(payload.interval) > 0 ? Number(payload.interval) : 5,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function parseOAuthTokenError(status, text) {
|
|
371
|
+
try {
|
|
372
|
+
const payload = JSON.parse(text);
|
|
373
|
+
const error = typeof payload.error === "string" && payload.error
|
|
374
|
+
? payload.error
|
|
375
|
+
: `token endpoint HTTP ${status}`;
|
|
376
|
+
const description = typeof payload.error_description === "string"
|
|
377
|
+
? payload.error_description
|
|
378
|
+
: undefined;
|
|
379
|
+
return new OAuthTokenEndpointError(status, error, description);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return new OAuthTokenEndpointError(status, `token endpoint HTTP ${status}`, text.slice(0, 500));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
314
385
|
export async function exchangeAuthorizationCode(opts) {
|
|
315
386
|
const body = new URLSearchParams();
|
|
316
387
|
body.set("grant_type", "authorization_code");
|
|
@@ -340,6 +411,33 @@ export async function exchangeAuthorizationCode(opts) {
|
|
|
340
411
|
}
|
|
341
412
|
return token;
|
|
342
413
|
}
|
|
414
|
+
export async function exchangeDeviceCode(opts) {
|
|
415
|
+
const body = new URLSearchParams();
|
|
416
|
+
body.set("grant_type", DEVICE_CODE_GRANT_TYPE);
|
|
417
|
+
body.set("client_id", opts.clientId);
|
|
418
|
+
body.set("device_code", opts.deviceCode);
|
|
419
|
+
if (opts.clientSecret)
|
|
420
|
+
body.set("client_secret", opts.clientSecret);
|
|
421
|
+
if (opts.resource)
|
|
422
|
+
body.set("resource", opts.resource);
|
|
423
|
+
const resp = await fetch(opts.tokenEndpoint, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: {
|
|
426
|
+
Accept: "application/json",
|
|
427
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
428
|
+
},
|
|
429
|
+
body,
|
|
430
|
+
});
|
|
431
|
+
const text = await resp.text();
|
|
432
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
433
|
+
throw parseOAuthTokenError(resp.status, text);
|
|
434
|
+
}
|
|
435
|
+
const token = JSON.parse(text);
|
|
436
|
+
if (!token.access_token) {
|
|
437
|
+
throw new Error("token endpoint response missing access_token");
|
|
438
|
+
}
|
|
439
|
+
return token;
|
|
440
|
+
}
|
|
343
441
|
export async function exchangeRefreshToken(opts) {
|
|
344
442
|
const body = new URLSearchParams();
|
|
345
443
|
body.set("grant_type", "refresh_token");
|
package/dist/oauthPending.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// ~/.tyc/oauth_pending.json 读写
|
|
2
2
|
//
|
|
3
|
-
// 非阻塞 OAuth
|
|
4
|
-
//
|
|
3
|
+
// 非阻塞 OAuth 登录会保存一次性上下文:
|
|
4
|
+
// - legacy PKCE 模式保存 code_verifier/state,靠 --callback-token 续接;
|
|
5
|
+
// - RFC 8628 device flow 保存 device_code,靠 --resume 续接。
|
|
5
6
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { join } from "node:path";
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tyc-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "天眼查 AI CLI — 162 个业务语义聚合工具 · 默认 Shared Core 调用 · 兼容 MCP transport",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tyc": "
|
|
7
|
+
"tyc": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc && cp src/catalog.json dist/catalog.json",
|