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 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:只打印授权 URL,不占住命令行;授权后手动传回 callback code
65
- tyc login --no-open --no-block
66
- tyc login --callback-token "<callback_code_or_full_callback_url>"
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-open --no-block` 会打印授权 URL 后立即退出,并把一次性 PKCE 上下文
81
- > 保存到 `~/.tyc/oauth_pending.json`;完成浏览器授权后,复制回调 URL 中的 `code`
82
- > 参数或完整 callback URL,执行
83
- > `tyc login --callback-token "<...>"` 完成换 token。
84
- > `tyc init` 保存配置后会立即校验 shared core endpoint;失败则退出码 1 并提示连通性问题。
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-open --no-block` | 打印 OAuth 授权 URL 并立即退出,等待后续手动完成 |
127
- | `tyc login --callback-token <code-or-url>` | 使用上一步保存的 PKCE 上下文,把回调 code 换成 access token |
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 |
@@ -1,9 +1,10 @@
1
- import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
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,并校验 shared core 连通性)")
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
- console.log(`已保存 ~/.tyc/config.json (url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
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 resolved = resolveConfig();
62
- const verbose = !!program.opts().verbose;
63
- await verifyCoreEndpoint(resolved, verbose);
64
- console.log("Shared Core endpoint 校验通过");
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(`连通性校验失败:${msg}`);
69
- console.error("配置已保存;确认 --url 可达后重新运行 tyc init,或加 --no-verify 跳过校验。");
94
+ console.error(`配置校验失败:${msg}`);
95
+ console.error("配置已保存;确认 --url 可达且 Authorization 有效后重新运行 tyc init,或加 --no-verify 跳过校验。");
70
96
  process.exit(1);
71
97
  }
72
98
  });
@@ -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", "配合 --no-open:打印授权 URL 并保存 pending 状态后立即退出")
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-open --no-block。");
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
- ? buildLoopbackRedirectUri(redirectHost, redirectPort, callbackPath)
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: 1,
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
- redirectUri,
244
- codeVerifier: pkce.codeVerifier,
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
- state,
334
+ scope,
247
335
  verify: opts.verify !== false,
248
336
  });
249
337
  console.error("请在浏览器打开以下 URL 完成登录:");
250
- console.error(authorizationURL);
338
+ console.error(device.verification_uri);
339
+ console.error("");
340
+ console.error(`验证码:${device.user_code}`);
251
341
  console.error("");
252
- console.error("完成授权后,复制回调 URL 中的 code 参数,然后运行:");
253
- console.error("tyc login --callback-token <code_or_callback_url>");
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-open --no-block 打印 OAuth URL 后立即退出,手动完成
71
- tyc login --callback-token <CODE_OR_CALLBACK_URL> 继续非阻塞 OAuth 登录
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");
@@ -1,7 +1,8 @@
1
1
  // ~/.tyc/oauth_pending.json 读写
2
2
  //
3
- // 非阻塞 OAuth 登录会先保存 PKCE code_verifier、state、client_id 等一次性上下文;
4
- // 用户完成浏览器授权后,再用 tyc login --callback-token <code_or_callback_url> 继续换取 access token。
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.6",
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": "./dist/index.js"
7
+ "tyc": "dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc && cp src/catalog.json dist/catalog.json",