tyc-cli 0.3.7 → 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 -9
- package/dist/commands/init.js +36 -10
- package/dist/commands/login.js +133 -26
- package/dist/index.js +3 -2
- package/dist/oauth.js +94 -0
- package/dist/oauthPending.js +3 -2
- package/package.json +1 -1
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,10 +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 init` 保存配置后会立即校验 shared core
|
|
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 并提示配置校验问题。
|
|
84
85
|
> 加 `--no-verify` 可跳过校验。
|
|
85
86
|
|
|
86
87
|
配置存于 `~/.tyc/config.json`(权限 600):
|
|
@@ -122,8 +123,9 @@ tyc executive personnel-dishonest "..." --humanName "张三"
|
|
|
122
123
|
| 命令 | 说明 |
|
|
123
124
|
|------|------|
|
|
124
125
|
| `tyc login` | 浏览器 OAuth 登录;自动发现 metadata、使用 PKCE 获取 access token / refresh token 并写入配置 |
|
|
125
|
-
| `tyc login --no-
|
|
126
|
-
| `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 |
|
|
127
129
|
| `tyc login --url <url>` | 对指定 MCP endpoint 发起 OAuth 登录 |
|
|
128
130
|
| `tyc init --authorization <token>` | 写入 `headers.Authorization`;保存后会立即校验 shared core 连通性 |
|
|
129
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, buildCliCallbackRedirectUri, 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) {
|
|
@@ -45,6 +45,15 @@ async function verifySavedOAuthLogin(verify, verbose) {
|
|
|
45
45
|
}
|
|
46
46
|
printLoginSuccessBanner();
|
|
47
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
|
+
}
|
|
48
57
|
async function completePendingOAuthLogin(pending, callbackToken, verify, verbose) {
|
|
49
58
|
const code = parseAuthorizationCallbackCode(callbackToken, pending.state);
|
|
50
59
|
if (verbose) {
|
|
@@ -68,6 +77,63 @@ async function completePendingOAuthLogin(pending, callbackToken, verify, verbose
|
|
|
68
77
|
clearPendingOAuthLogin(verbose);
|
|
69
78
|
await verifySavedOAuthLogin(verify, verbose);
|
|
70
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
|
+
}
|
|
71
137
|
export function registerLoginCommand(program) {
|
|
72
138
|
program
|
|
73
139
|
.command("login")
|
|
@@ -81,21 +147,40 @@ export function registerLoginCommand(program) {
|
|
|
81
147
|
.option("--resource-metadata-url <url>", "手动指定 MCP protected resource metadata URL")
|
|
82
148
|
.option("--authorization-endpoint <url>", "手动指定 OAuth authorization endpoint")
|
|
83
149
|
.option("--token-endpoint <url>", "手动指定 OAuth token endpoint")
|
|
150
|
+
.option("--device-authorization-endpoint <url>", "手动指定 OAuth device authorization endpoint")
|
|
84
151
|
.option("--redirect-host <host>", "本地回调 host(默认 localhost)")
|
|
85
152
|
.option("--redirect-port <port>", "本地回调端口(默认 7078)")
|
|
86
153
|
.option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
|
|
87
154
|
.option("--callback-token <code-or-url>", "继续非阻塞登录:传入授权回调 code 或包含 code 的完整 callback URL")
|
|
88
155
|
.option("--token <code-or-url>", "同 --callback-token(兼容别名;仍需传授权回调 code)")
|
|
156
|
+
.option("--resume", "继续 RFC 8628 device flow 非阻塞登录")
|
|
89
157
|
.option("--no-open", "不自动打开浏览器,只打印授权 URL")
|
|
90
|
-
.option("--no-block", "
|
|
158
|
+
.option("--no-block", "打印 Device Flow 授权 URL 和 6 位验证码并保存 pending 状态后立即退出")
|
|
91
159
|
.option("--no-verify", "保存 token 后跳过 shared core 鉴权校验")
|
|
92
160
|
.action(async (opts) => {
|
|
93
161
|
const verbose = !!program.opts().verbose;
|
|
94
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
|
+
}
|
|
95
177
|
if (callbackToken) {
|
|
96
178
|
const pending = loadPendingOAuthLogin();
|
|
97
179
|
if (!pending) {
|
|
98
|
-
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。");
|
|
99
184
|
}
|
|
100
185
|
if (verbose) {
|
|
101
186
|
console.error(`> loaded pending OAuth login from ~/.tyc/oauth_pending.json (url=${pending.mcpURL})`);
|
|
@@ -104,9 +189,6 @@ export function registerLoginCommand(program) {
|
|
|
104
189
|
return;
|
|
105
190
|
}
|
|
106
191
|
const noBlock = opts.block === false;
|
|
107
|
-
if (noBlock && opts.open !== false) {
|
|
108
|
-
throw new Error("--no-block 需要与 --no-open 一起使用。");
|
|
109
|
-
}
|
|
110
192
|
const current = loadConfig() || {};
|
|
111
193
|
const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
|
|
112
194
|
const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
|
|
@@ -158,6 +240,7 @@ export function registerLoginCommand(program) {
|
|
|
158
240
|
endpoints = {
|
|
159
241
|
authorizationEndpoint: opts.authorizationEndpoint,
|
|
160
242
|
tokenEndpoint: opts.tokenEndpoint,
|
|
243
|
+
deviceAuthorizationEndpoint: opts.deviceAuthorizationEndpoint,
|
|
161
244
|
issuer,
|
|
162
245
|
};
|
|
163
246
|
}
|
|
@@ -166,11 +249,16 @@ export function registerLoginCommand(program) {
|
|
|
166
249
|
endpoints.authorizationEndpoint =
|
|
167
250
|
opts.authorizationEndpoint || endpoints.authorizationEndpoint;
|
|
168
251
|
endpoints.tokenEndpoint = opts.tokenEndpoint || endpoints.tokenEndpoint;
|
|
252
|
+
endpoints.deviceAuthorizationEndpoint =
|
|
253
|
+
opts.deviceAuthorizationEndpoint || endpoints.deviceAuthorizationEndpoint;
|
|
169
254
|
}
|
|
170
255
|
resource ||= endpoints.resourceDocumentation || mcpURL;
|
|
171
256
|
if (verbose) {
|
|
172
257
|
console.error(`> authorization endpoint: ${endpoints.authorizationEndpoint}`);
|
|
173
258
|
console.error(`> token endpoint: ${endpoints.tokenEndpoint}`);
|
|
259
|
+
if (endpoints.deviceAuthorizationEndpoint) {
|
|
260
|
+
console.error(`> device authorization endpoint: ${endpoints.deviceAuthorizationEndpoint}`);
|
|
261
|
+
}
|
|
174
262
|
if (endpoints.registrationEndpoint) {
|
|
175
263
|
console.error(`> registration endpoint: ${endpoints.registrationEndpoint}`);
|
|
176
264
|
}
|
|
@@ -201,6 +289,10 @@ export function registerLoginCommand(program) {
|
|
|
201
289
|
registrationEndpoint: endpoints.registrationEndpoint,
|
|
202
290
|
clientName: "tyc-cli",
|
|
203
291
|
redirectUris: [redirectUri],
|
|
292
|
+
grantTypes: noBlock
|
|
293
|
+
? ["authorization_code", "refresh_token", DEVICE_CODE_GRANT_TYPE]
|
|
294
|
+
: undefined,
|
|
295
|
+
responseTypes: ["code"],
|
|
204
296
|
tokenEndpointAuthMethod: "none",
|
|
205
297
|
});
|
|
206
298
|
}
|
|
@@ -214,40 +306,55 @@ export function registerLoginCommand(program) {
|
|
|
214
306
|
console.error(`> registered client_id: ${clientId}`);
|
|
215
307
|
}
|
|
216
308
|
}
|
|
217
|
-
const authorizationURL = buildAuthorizationURL({
|
|
218
|
-
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
219
|
-
clientId,
|
|
220
|
-
redirectUri,
|
|
221
|
-
scope,
|
|
222
|
-
state,
|
|
223
|
-
codeChallenge: pkce.codeChallenge,
|
|
224
|
-
resource,
|
|
225
|
-
});
|
|
226
|
-
if (verbose) {
|
|
227
|
-
console.error(`> OAuth callback: ${redirectUri}`);
|
|
228
|
-
}
|
|
229
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
|
+
});
|
|
230
320
|
savePendingOAuthLogin({
|
|
231
|
-
version:
|
|
321
|
+
version: 2,
|
|
322
|
+
flow: "device",
|
|
232
323
|
createdAt: Date.now(),
|
|
233
324
|
mcpURL,
|
|
234
325
|
tokenEndpoint: endpoints.tokenEndpoint,
|
|
235
326
|
clientId,
|
|
236
327
|
clientSecret,
|
|
237
|
-
|
|
238
|
-
|
|
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,
|
|
239
333
|
resource,
|
|
240
|
-
|
|
334
|
+
scope,
|
|
241
335
|
verify: opts.verify !== false,
|
|
242
336
|
});
|
|
243
337
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
244
|
-
console.error(
|
|
338
|
+
console.error(device.verification_uri);
|
|
245
339
|
console.error("");
|
|
246
|
-
console.error(
|
|
247
|
-
console.error("
|
|
248
|
-
console.error(
|
|
340
|
+
console.error(`验证码:${device.user_code}`);
|
|
341
|
+
console.error("");
|
|
342
|
+
console.error("在网页输入 6 位验证码并确认授权后,运行:");
|
|
343
|
+
console.error("tyc login --resume");
|
|
249
344
|
return;
|
|
250
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
|
+
}
|
|
251
358
|
if (opts.open === false) {
|
|
252
359
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
253
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,
|
|
@@ -315,6 +334,54 @@ export async function registerDynamicOAuthClient(opts) {
|
|
|
315
334
|
clientSecret: registered.client_secret,
|
|
316
335
|
};
|
|
317
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
|
+
}
|
|
318
385
|
export async function exchangeAuthorizationCode(opts) {
|
|
319
386
|
const body = new URLSearchParams();
|
|
320
387
|
body.set("grant_type", "authorization_code");
|
|
@@ -344,6 +411,33 @@ export async function exchangeAuthorizationCode(opts) {
|
|
|
344
411
|
}
|
|
345
412
|
return token;
|
|
346
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
|
+
}
|
|
347
441
|
export async function exchangeRefreshToken(opts) {
|
|
348
442
|
const body = new URLSearchParams();
|
|
349
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";
|