@bigbrain-work/mcp-connect 1.2.2 → 1.3.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.
- package/LICENSE +21 -0
- package/README.md +33 -59
- package/bin/mcp-connect.js +4 -4
- package/bin/shiliu.js +8 -0
- package/package.json +9 -5
- package/src/arguments.js +75 -19
- package/src/authorization.js +49 -0
- package/src/cli.js +424 -73
- package/src/configurators.js +118 -137
- package/src/constants.js +12 -6
- package/src/credentials.js +166 -148
- package/src/detection.js +16 -21
- package/src/device-auth-client.js +130 -0
- package/src/login-flow.js +39 -0
- package/src/proxy.js +59 -58
- package/src/remote-client.js +27 -10
- package/src/security.js +63 -0
- package/src/status.js +117 -58
- package/src/token-store.js +116 -0
- package/src/updater.js +59 -0
package/src/proxy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Server } from
|
|
2
|
-
import { StdioServerTransport } from
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import {
|
|
4
4
|
CallToolRequestSchema,
|
|
5
5
|
GetPromptRequestSchema,
|
|
@@ -8,85 +8,86 @@ import {
|
|
|
8
8
|
ListResourceTemplatesRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
ReadResourceRequestSchema,
|
|
11
|
-
} from
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { connectRemote } from
|
|
16
|
-
|
|
17
|
-
function resolveProxyApiKey({ home, platform = process.platform, env = process.env }) {
|
|
18
|
-
const apiKey = readPersistedApiKey({ home, platform, env })?.trim()
|
|
19
|
-
if (!apiKey) {
|
|
20
|
-
throw new Error(`未找到 API Key,请先运行 mcp-connect 安装,或设置 ${API_KEY_ENV}`)
|
|
21
|
-
}
|
|
22
|
-
return apiKey
|
|
23
|
-
}
|
|
13
|
+
import { resolveAuthorization } from "./authorization.js";
|
|
14
|
+
import { API_KEY_ENV, AUTH_URL, PACKAGE_VERSION } from "./constants.js";
|
|
15
|
+
import { connectRemote } from "./remote-client.js";
|
|
24
16
|
|
|
25
17
|
export async function runProxy({
|
|
26
18
|
home,
|
|
27
19
|
url,
|
|
20
|
+
authUrl = AUTH_URL,
|
|
28
21
|
platform = process.platform,
|
|
29
22
|
env = process.env,
|
|
30
23
|
} = {}) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
24
|
+
const authorization = await resolveAuthorization({
|
|
25
|
+
home,
|
|
26
|
+
platform,
|
|
27
|
+
env,
|
|
28
|
+
authUrl,
|
|
29
|
+
});
|
|
30
|
+
if (!authorization.token) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`未找到登录凭据,请先运行 shiliu login,或设置 ${API_KEY_ENV}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const remote = await connectRemote({ token: authorization.token, url });
|
|
36
|
+
const remoteCapabilities = remote.getServerCapabilities() ?? {};
|
|
37
|
+
const capabilities = {};
|
|
38
|
+
if (remoteCapabilities.tools) capabilities.tools = {};
|
|
39
|
+
if (remoteCapabilities.resources) capabilities.resources = {};
|
|
40
|
+
if (remoteCapabilities.prompts) capabilities.prompts = {};
|
|
38
41
|
|
|
39
42
|
const server = new Server(
|
|
40
|
-
{
|
|
41
|
-
name: 'shiliu-ai-mcp-proxy',
|
|
42
|
-
version: PACKAGE_VERSION,
|
|
43
|
-
},
|
|
43
|
+
{ name: "shiliu-ai-mcp-proxy", version: PACKAGE_VERSION },
|
|
44
44
|
{
|
|
45
45
|
capabilities,
|
|
46
|
-
instructions:
|
|
46
|
+
instructions:
|
|
47
|
+
"Authenticated stdio bridge for the Shiliu AI remote MCP service.",
|
|
47
48
|
},
|
|
48
|
-
)
|
|
49
|
+
);
|
|
49
50
|
|
|
50
51
|
if (remoteCapabilities.tools) {
|
|
51
|
-
server.setRequestHandler(ListToolsRequestSchema, (request) =>
|
|
52
|
-
remote.listTools(request.params)
|
|
53
|
-
)
|
|
54
|
-
server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
55
|
-
remote.callTool(request.params)
|
|
56
|
-
)
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, (request) =>
|
|
53
|
+
remote.listTools(request.params),
|
|
54
|
+
);
|
|
55
|
+
server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
56
|
+
remote.callTool(request.params),
|
|
57
|
+
);
|
|
57
58
|
}
|
|
58
59
|
if (remoteCapabilities.resources) {
|
|
59
|
-
server.setRequestHandler(ListResourcesRequestSchema, (request) =>
|
|
60
|
-
remote.listResources(request.params)
|
|
61
|
-
)
|
|
62
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) =>
|
|
63
|
-
remote.listResourceTemplates(request.params)
|
|
64
|
-
)
|
|
65
|
-
server.setRequestHandler(ReadResourceRequestSchema, (request) =>
|
|
66
|
-
remote.readResource(request.params)
|
|
67
|
-
)
|
|
60
|
+
server.setRequestHandler(ListResourcesRequestSchema, (request) =>
|
|
61
|
+
remote.listResources(request.params),
|
|
62
|
+
);
|
|
63
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) =>
|
|
64
|
+
remote.listResourceTemplates(request.params),
|
|
65
|
+
);
|
|
66
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) =>
|
|
67
|
+
remote.readResource(request.params),
|
|
68
|
+
);
|
|
68
69
|
}
|
|
69
70
|
if (remoteCapabilities.prompts) {
|
|
70
|
-
server.setRequestHandler(ListPromptsRequestSchema, (request) =>
|
|
71
|
-
remote.listPrompts(request.params)
|
|
72
|
-
)
|
|
73
|
-
server.setRequestHandler(GetPromptRequestSchema, (request) =>
|
|
74
|
-
remote.getPrompt(request.params)
|
|
75
|
-
)
|
|
71
|
+
server.setRequestHandler(ListPromptsRequestSchema, (request) =>
|
|
72
|
+
remote.listPrompts(request.params),
|
|
73
|
+
);
|
|
74
|
+
server.setRequestHandler(GetPromptRequestSchema, (request) =>
|
|
75
|
+
remote.getPrompt(request.params),
|
|
76
|
+
);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
let closing = false
|
|
79
|
+
let closing = false;
|
|
79
80
|
const shutdown = async () => {
|
|
80
|
-
if (closing) return
|
|
81
|
-
closing = true
|
|
82
|
-
await Promise.allSettled([server.close(), remote.close()])
|
|
83
|
-
}
|
|
84
|
-
process.once(
|
|
85
|
-
process.once(
|
|
81
|
+
if (closing) return;
|
|
82
|
+
closing = true;
|
|
83
|
+
await Promise.allSettled([server.close(), remote.close()]);
|
|
84
|
+
};
|
|
85
|
+
process.once("SIGINT", shutdown);
|
|
86
|
+
process.once("SIGTERM", shutdown);
|
|
86
87
|
try {
|
|
87
|
-
await server.connect(new StdioServerTransport())
|
|
88
|
+
await server.connect(new StdioServerTransport());
|
|
88
89
|
} catch (error) {
|
|
89
|
-
await shutdown()
|
|
90
|
-
throw error
|
|
90
|
+
await shutdown();
|
|
91
|
+
throw error;
|
|
91
92
|
}
|
|
92
93
|
}
|
package/src/remote-client.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
import { Client } from
|
|
2
|
-
import { StreamableHTTPClientTransport } from
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
3
|
|
|
4
|
-
import { PACKAGE_VERSION } from
|
|
4
|
+
import { PACKAGE_VERSION } from "./constants.js";
|
|
5
5
|
|
|
6
|
-
export async function connectRemote({ apiKey, url }) {
|
|
6
|
+
export async function connectRemote({ token, apiKey, url }) {
|
|
7
|
+
const authorizationToken = token || apiKey;
|
|
8
|
+
if (!authorizationToken) throw new Error("Missing authorization token");
|
|
7
9
|
const client = new Client({
|
|
8
|
-
name:
|
|
10
|
+
name: "shiliu-ai-cli",
|
|
9
11
|
version: PACKAGE_VERSION,
|
|
10
|
-
})
|
|
12
|
+
});
|
|
11
13
|
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
12
14
|
requestInit: {
|
|
13
15
|
headers: {
|
|
14
|
-
Authorization: `Bearer ${
|
|
16
|
+
Authorization: `Bearer ${authorizationToken}`,
|
|
15
17
|
},
|
|
16
18
|
},
|
|
17
|
-
})
|
|
18
|
-
await client.connect(transport)
|
|
19
|
-
return client
|
|
19
|
+
});
|
|
20
|
+
await client.connect(transport);
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readToolCatalog({
|
|
25
|
+
token,
|
|
26
|
+
apiKey,
|
|
27
|
+
url,
|
|
28
|
+
connect = connectRemote,
|
|
29
|
+
}) {
|
|
30
|
+
const client = await connect({ token: token || apiKey, url });
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.listTools();
|
|
33
|
+
return result.tools || [];
|
|
34
|
+
} finally {
|
|
35
|
+
await client.close();
|
|
36
|
+
}
|
|
20
37
|
}
|
package/src/security.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AUTH_URL, MCP_URL } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
4
|
+
const USER_CODE_PATTERN = /^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/u;
|
|
5
|
+
|
|
6
|
+
function normalizeEndpoint(value) {
|
|
7
|
+
const parsed = new URL(value);
|
|
8
|
+
if (parsed.username || parsed.password) {
|
|
9
|
+
throw new Error("服务地址不能包含用户名或密码");
|
|
10
|
+
}
|
|
11
|
+
if (parsed.hash) throw new Error("服务地址不能包含 URL 片段");
|
|
12
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isSameEndpoint(actual, expected) {
|
|
17
|
+
return (
|
|
18
|
+
actual.protocol === expected.protocol &&
|
|
19
|
+
actual.hostname === expected.hostname &&
|
|
20
|
+
actual.port === expected.port &&
|
|
21
|
+
actual.pathname === expected.pathname &&
|
|
22
|
+
actual.search === expected.search
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateServiceUrl(value, expectedValue, label, { allowLocalhost }) {
|
|
27
|
+
let actual;
|
|
28
|
+
let expected;
|
|
29
|
+
try {
|
|
30
|
+
actual = normalizeEndpoint(value);
|
|
31
|
+
expected = normalizeEndpoint(expectedValue);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`${label}无效:${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isSameEndpoint(actual, expected)) return actual.toString();
|
|
37
|
+
if (
|
|
38
|
+
allowLocalhost &&
|
|
39
|
+
LOOPBACK_HOSTS.has(actual.hostname) &&
|
|
40
|
+
["http:", "https:"].includes(actual.protocol)
|
|
41
|
+
) {
|
|
42
|
+
return actual.toString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${label}仅允许石榴 AI 正式地址;本机测试请使用回环地址并显式添加 --allow-localhost`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateMcpUrl(value, options = {}) {
|
|
51
|
+
return validateServiceUrl(value, MCP_URL, "MCP 地址", options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateAuthUrl(value, options = {}) {
|
|
55
|
+
return validateServiceUrl(value, AUTH_URL, "授权地址", options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function normalizeDeviceUserCode(value) {
|
|
59
|
+
if (typeof value !== "string" || !USER_CODE_PATTERN.test(value)) {
|
|
60
|
+
throw new Error("授权服务返回的登录会话编号格式无效");
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
package/src/status.js
CHANGED
|
@@ -1,94 +1,153 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import path from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AUTH_URL, MCP_URL, PACKAGE_NAME, SERVER_NAME } from "./constants.js";
|
|
5
|
+
import { resolveAuthorization } from "./authorization.js";
|
|
6
|
+
import { readToolCatalog } from "./remote-client.js";
|
|
6
7
|
|
|
7
8
|
async function readText(filePath) {
|
|
8
9
|
try {
|
|
9
|
-
return await readFile(filePath,
|
|
10
|
+
return await readFile(filePath, "utf8");
|
|
10
11
|
} catch (error) {
|
|
11
|
-
if (error.code ===
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
throw error
|
|
12
|
+
if (error.code === "ENOENT") return "";
|
|
13
|
+
throw error;
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
async function hasServerConfig(filePath) {
|
|
19
|
-
const content = await readText(filePath)
|
|
20
|
-
return
|
|
18
|
+
const content = await readText(filePath);
|
|
19
|
+
return (
|
|
20
|
+
content.includes(SERVER_NAME) &&
|
|
21
|
+
(content.includes(MCP_URL) || content.includes(PACKAGE_NAME))
|
|
22
|
+
);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export async function inspectLocalConfiguration(home) {
|
|
24
26
|
return {
|
|
25
|
-
codex: await hasServerConfig(path.join(home,
|
|
26
|
-
claude: await hasServerConfig(path.join(home,
|
|
27
|
-
cursor: await hasServerConfig(path.join(home,
|
|
28
|
-
}
|
|
27
|
+
codex: await hasServerConfig(path.join(home, ".codex", "config.toml")),
|
|
28
|
+
claude: await hasServerConfig(path.join(home, ".claude.json")),
|
|
29
|
+
cursor: await hasServerConfig(path.join(home, ".cursor", "mcp.json")),
|
|
30
|
+
};
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export async function probeMcp(
|
|
32
|
-
|
|
33
|
+
export async function probeMcp(
|
|
34
|
+
token,
|
|
35
|
+
{ url = MCP_URL, readCatalog = readToolCatalog } = {},
|
|
36
|
+
) {
|
|
37
|
+
if (!token) {
|
|
33
38
|
return {
|
|
34
39
|
ok: false,
|
|
35
|
-
detail:
|
|
36
|
-
|
|
40
|
+
detail: "尚未登录",
|
|
41
|
+
toolCount: 0,
|
|
42
|
+
tools: [],
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
try {
|
|
40
|
-
const
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
Accept: 'application/json, text/event-stream',
|
|
44
|
-
Authorization: `Bearer ${apiKey}`,
|
|
45
|
-
'Content-Type': 'application/json',
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify({
|
|
48
|
-
jsonrpc: '2.0',
|
|
49
|
-
id: 1,
|
|
50
|
-
method: 'initialize',
|
|
51
|
-
params: {
|
|
52
|
-
protocolVersion: '2025-03-26',
|
|
53
|
-
capabilities: {},
|
|
54
|
-
clientInfo: {
|
|
55
|
-
name: 'bigbrain-work-mcp-connect',
|
|
56
|
-
version: '1.0.0',
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
}),
|
|
60
|
-
signal: AbortSignal.timeout(10000),
|
|
61
|
-
})
|
|
47
|
+
const tools = await readCatalog({ token, url });
|
|
62
48
|
return {
|
|
63
|
-
ok:
|
|
64
|
-
detail:
|
|
65
|
-
|
|
49
|
+
ok: true,
|
|
50
|
+
detail: `连接正常,tools/list 返回 ${tools.length} 个工具`,
|
|
51
|
+
toolCount: tools.length,
|
|
52
|
+
tools,
|
|
53
|
+
};
|
|
66
54
|
} catch (error) {
|
|
67
55
|
return {
|
|
68
56
|
ok: false,
|
|
69
57
|
detail: `连接失败:${error.message}`,
|
|
70
|
-
|
|
58
|
+
toolCount: 0,
|
|
59
|
+
tools: [],
|
|
60
|
+
};
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
63
|
|
|
74
64
|
function mark(ok) {
|
|
75
|
-
return ok ?
|
|
65
|
+
return ok ? "✓" : "○";
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
export async function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
export async function getStatus({
|
|
69
|
+
home,
|
|
70
|
+
platform = process.platform,
|
|
71
|
+
env = process.env,
|
|
72
|
+
url = MCP_URL,
|
|
73
|
+
authUrl = AUTH_URL,
|
|
74
|
+
readCatalog,
|
|
75
|
+
resolveAuth = resolveAuthorization,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const authorization = await resolveAuth({ home, platform, env, authUrl });
|
|
78
|
+
const configs = await inspectLocalConfiguration(home);
|
|
79
|
+
const remote = await probeMcp(authorization.token, { url, readCatalog });
|
|
89
80
|
return {
|
|
90
|
-
|
|
81
|
+
credential: Boolean(authorization.token),
|
|
82
|
+
credentialSource: authorization.source,
|
|
91
83
|
configs,
|
|
92
84
|
remote,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function printStatus(options = {}) {
|
|
89
|
+
const result = await getStatus(options);
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(
|
|
92
|
+
JSON.stringify(
|
|
93
|
+
{
|
|
94
|
+
credential: result.credential,
|
|
95
|
+
credentialSource: result.credentialSource,
|
|
96
|
+
configs: result.configs,
|
|
97
|
+
remote: {
|
|
98
|
+
ok: result.remote.ok,
|
|
99
|
+
detail: result.remote.detail,
|
|
100
|
+
toolCount: result.remote.toolCount,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(
|
|
111
|
+
`登录凭据 ${mark(result.credential)} ${result.credential ? `已设置(${result.credentialSource})` : "未设置"}`,
|
|
112
|
+
);
|
|
113
|
+
console.log(
|
|
114
|
+
`Codex 配置 ${mark(result.configs.codex)} ${result.configs.codex ? "已写入" : "未发现"}`,
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
`Claude Code 配置 ${mark(result.configs.claude)} ${result.configs.claude ? "已写入" : "未发现"}`,
|
|
118
|
+
);
|
|
119
|
+
console.log(
|
|
120
|
+
`Cursor 配置 ${mark(result.configs.cursor)} ${result.configs.cursor ? "已写入" : "未发现"}`,
|
|
121
|
+
);
|
|
122
|
+
console.log(
|
|
123
|
+
`石榴 AI MCP ${mark(result.remote.ok)} ${result.remote.detail}`,
|
|
124
|
+
);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function printTools({
|
|
129
|
+
home,
|
|
130
|
+
platform = process.platform,
|
|
131
|
+
env = process.env,
|
|
132
|
+
url = MCP_URL,
|
|
133
|
+
authUrl = AUTH_URL,
|
|
134
|
+
json = false,
|
|
135
|
+
readCatalog = readToolCatalog,
|
|
136
|
+
resolveAuth = resolveAuthorization,
|
|
137
|
+
} = {}) {
|
|
138
|
+
const authorization = await resolveAuth({ home, platform, env, authUrl });
|
|
139
|
+
if (!authorization.token) throw new Error("尚未登录,请先运行 shiliu login");
|
|
140
|
+
const tools = await readCatalog({ token: authorization.token, url });
|
|
141
|
+
|
|
142
|
+
if (json) {
|
|
143
|
+
console.log(JSON.stringify({ count: tools.length, tools }, null, 2));
|
|
144
|
+
return tools;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`石榴 AI MCP 当前提供 ${tools.length} 个工具:`);
|
|
148
|
+
for (const tool of tools) {
|
|
149
|
+
const summary = tool.description ? ` — ${tool.description}` : "";
|
|
150
|
+
console.log(`- ${tool.name}${summary}`);
|
|
93
151
|
}
|
|
152
|
+
return tools;
|
|
94
153
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KEYRING_ACCOUNT,
|
|
3
|
+
KEYRING_SERVICE,
|
|
4
|
+
PENDING_KEYRING_ACCOUNT,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
|
|
7
|
+
function validateTokenSet(value) {
|
|
8
|
+
if (
|
|
9
|
+
!value ||
|
|
10
|
+
typeof value !== "object" ||
|
|
11
|
+
typeof value.accessToken !== "string" ||
|
|
12
|
+
typeof value.refreshToken !== "string" ||
|
|
13
|
+
typeof value.expiresAt !== "number"
|
|
14
|
+
) {
|
|
15
|
+
throw new Error("本机凭据格式无效,请重新运行 shiliu login");
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createSystemEntry(account) {
|
|
21
|
+
let module;
|
|
22
|
+
try {
|
|
23
|
+
module = await import("@napi-rs/keyring");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`无法加载系统凭据库:${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
return new module.Entry(KEYRING_SERVICE, account);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function defaultEntryFactory() {
|
|
31
|
+
return createSystemEntry(KEYRING_ACCOUNT);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function defaultPendingEntryFactory() {
|
|
35
|
+
return createSystemEntry(PENDING_KEYRING_ACCOUNT);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class TokenStore {
|
|
39
|
+
constructor({ entryFactory = defaultEntryFactory } = {}) {
|
|
40
|
+
this.entryFactory = entryFactory;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async save(tokenSet) {
|
|
44
|
+
const entry = await this.entryFactory();
|
|
45
|
+
await entry.setPassword(JSON.stringify(validateTokenSet(tokenSet)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async load() {
|
|
49
|
+
const entry = await this.entryFactory();
|
|
50
|
+
try {
|
|
51
|
+
const serialized = await entry.getPassword();
|
|
52
|
+
if (!serialized) return null;
|
|
53
|
+
return validateTokenSet(JSON.parse(serialized));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (this.isMissingEntry(error)) return null;
|
|
56
|
+
throw new Error(`无法读取系统凭据库:${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async clear() {
|
|
61
|
+
const entry = await this.entryFactory();
|
|
62
|
+
try {
|
|
63
|
+
await entry.deletePassword();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!this.isMissingEntry(error)) {
|
|
66
|
+
throw new Error(`无法清除系统凭据库:${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isMissingEntry(error) {
|
|
72
|
+
return /not found|no entry|no matching|credential.*missing/iu.test(
|
|
73
|
+
error?.message || "",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validatePendingLogin(value) {
|
|
79
|
+
if (
|
|
80
|
+
!value ||
|
|
81
|
+
typeof value !== "object" ||
|
|
82
|
+
typeof value.sessionId !== "string" ||
|
|
83
|
+
typeof value.deviceCode !== "string" ||
|
|
84
|
+
typeof value.expiresAt !== "number" ||
|
|
85
|
+
typeof value.interval !== "number" ||
|
|
86
|
+
typeof value.authUrl !== "string" ||
|
|
87
|
+
(value.allowLocalhost !== undefined &&
|
|
88
|
+
typeof value.allowLocalhost !== "boolean")
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("待处理登录会话格式无效,请重新运行 shiliu login");
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class PendingLoginStore extends TokenStore {
|
|
96
|
+
constructor({ entryFactory = defaultPendingEntryFactory } = {}) {
|
|
97
|
+
super({ entryFactory });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async save(value) {
|
|
101
|
+
const entry = await this.entryFactory();
|
|
102
|
+
await entry.setPassword(JSON.stringify(validatePendingLogin(value)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async load() {
|
|
106
|
+
const entry = await this.entryFactory();
|
|
107
|
+
try {
|
|
108
|
+
const serialized = await entry.getPassword();
|
|
109
|
+
if (!serialized) return null;
|
|
110
|
+
return validatePendingLogin(JSON.parse(serialized));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (this.isMissingEntry(error)) return null;
|
|
113
|
+
throw new Error(`无法读取待处理登录会话:${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PACKAGE_NAME,
|
|
3
|
+
PACKAGE_VERSION,
|
|
4
|
+
REGISTRY_LATEST_URL,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
|
|
7
|
+
function parseVersion(value) {
|
|
8
|
+
const match = String(value).match(
|
|
9
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u,
|
|
10
|
+
);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
return {
|
|
13
|
+
numbers: match.slice(1, 4).map(Number),
|
|
14
|
+
prerelease: match[4] || "",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function compareVersions(left, right) {
|
|
19
|
+
const a = parseVersion(left);
|
|
20
|
+
const b = parseVersion(right);
|
|
21
|
+
if (!a || !b) return 0;
|
|
22
|
+
for (let index = 0; index < 3; index += 1) {
|
|
23
|
+
if (a.numbers[index] !== b.numbers[index]) {
|
|
24
|
+
return a.numbers[index] > b.numbers[index] ? 1 : -1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
28
|
+
if (!a.prerelease) return 1;
|
|
29
|
+
if (!b.prerelease) return -1;
|
|
30
|
+
return a.prerelease.localeCompare(b.prerelease);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function checkForUpdates({ fetchImpl = fetch } = {}) {
|
|
34
|
+
const response = await fetchImpl(REGISTRY_LATEST_URL, {
|
|
35
|
+
headers: { Accept: "application/json" },
|
|
36
|
+
signal: AbortSignal.timeout(10000),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new Error(`npm registry 返回 HTTP ${response.status}`);
|
|
40
|
+
const metadata = await response.json();
|
|
41
|
+
const latest = metadata.version;
|
|
42
|
+
if (!latest) throw new Error("npm registry 未返回版本号");
|
|
43
|
+
return {
|
|
44
|
+
current: PACKAGE_VERSION,
|
|
45
|
+
latest,
|
|
46
|
+
updateAvailable: compareVersions(latest, PACKAGE_VERSION) > 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function printUpdateStatus(options = {}) {
|
|
51
|
+
const result = await checkForUpdates(options);
|
|
52
|
+
if (result.updateAvailable) {
|
|
53
|
+
console.log(`发现新版本:${result.current} → ${result.latest}`);
|
|
54
|
+
console.log(`更新命令:npm install -g ${PACKAGE_NAME}@latest`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`当前版本:${result.current};npm latest:${result.latest}`);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|