@openlibing/openlibing-cli 0.1.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.
Files changed (53) hide show
  1. package/README.md +137 -0
  2. package/dist/auth/browser.d.ts +29 -0
  3. package/dist/auth/browser.js +123 -0
  4. package/dist/auth/browser.js.map +1 -0
  5. package/dist/auth/login.d.ts +30 -0
  6. package/dist/auth/login.js +180 -0
  7. package/dist/auth/login.js.map +1 -0
  8. package/dist/auth/refresh.d.ts +3 -0
  9. package/dist/auth/refresh.js +36 -0
  10. package/dist/auth/refresh.js.map +1 -0
  11. package/dist/auth/store.d.ts +19 -0
  12. package/dist/auth/store.js +71 -0
  13. package/dist/auth/store.js.map +1 -0
  14. package/dist/client/http.d.ts +15 -0
  15. package/dist/client/http.js +65 -0
  16. package/dist/client/http.js.map +1 -0
  17. package/dist/client/reportApi.d.ts +54 -0
  18. package/dist/client/reportApi.js +54 -0
  19. package/dist/client/reportApi.js.map +1 -0
  20. package/dist/commands/auth.d.ts +5 -0
  21. package/dist/commands/auth.js +56 -0
  22. package/dist/commands/auth.js.map +1 -0
  23. package/dist/commands/mcp.d.ts +5 -0
  24. package/dist/commands/mcp.js +33 -0
  25. package/dist/commands/mcp.js.map +1 -0
  26. package/dist/config/env.d.ts +19 -0
  27. package/dist/config/env.js +29 -0
  28. package/dist/config/env.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +19 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/mcp/registry.d.ts +2 -0
  33. package/dist/mcp/registry.js +8 -0
  34. package/dist/mcp/registry.js.map +1 -0
  35. package/dist/mcp/server.d.ts +12 -0
  36. package/dist/mcp/server.js +25 -0
  37. package/dist/mcp/server.js.map +1 -0
  38. package/dist/tools/auth/login.d.ts +4 -0
  39. package/dist/tools/auth/login.js +28 -0
  40. package/dist/tools/auth/login.js.map +1 -0
  41. package/dist/tools/report/index.d.ts +6 -0
  42. package/dist/tools/report/index.js +4 -0
  43. package/dist/tools/report/index.js.map +1 -0
  44. package/dist/tools/report/listTemplates.d.ts +2 -0
  45. package/dist/tools/report/listTemplates.js +16 -0
  46. package/dist/tools/report/listTemplates.js.map +1 -0
  47. package/dist/tools/report/queryData.d.ts +37 -0
  48. package/dist/tools/report/queryData.js +72 -0
  49. package/dist/tools/report/queryData.js.map +1 -0
  50. package/dist/tools/types.d.ts +18 -0
  51. package/dist/tools/types.js +7 -0
  52. package/dist/tools/types.js.map +1 -0
  53. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @openlibing/openlibing-cli
2
+
3
+ OpenLibing MCP 本地 CLI:把 ops 报告等只读能力包装为 MCP 工具(stdio 传输),供 Trae / OpenCode 等 AI 客户端直接调用。
4
+
5
+ - **源码仓库**:<https://gitcode.com/w30060144/openlibing-cli>
6
+ - **npm**:<https://www.npmjs.com/package/@openlibing/openlibing-cli>
7
+ - 发布维护:`@openlibing/openlibing-cli`(npmjs latest)
8
+
9
+ ## 特性
10
+
11
+ - **MCP 工具**(stdio):`report_listTemplates` / `report_queryData` / `auth_login`
12
+ - **连接即授权**:`mcp start` 启动时自动续期凭证;凭证失效自动弹出浏览器授权,**点一次即完成**
13
+ - **平台选择**:登录时弹出平台选择页(GitCode / Gitee / GitHub / UniPortal / OpenUBMC),点选后跳转对应平台授权
14
+ - **凭证安全**:token/refresh/csrf 本地 AES-256-GCM 加密存储(`~/.openlibing-cli/auth.json`),不落服务端
15
+ - **401 自愈**:工具调用遇 401 自动 refresh 续期重试;续期失败返回明确引导(不依赖外部 cookie)
16
+
17
+ ## 安装
18
+
19
+ ```powershell
20
+ # 直接通过 npx 使用(推荐,AI 客户端配置方式)
21
+ npx -y @openlibing/openlibing-cli --version
22
+
23
+ # 或全局安装
24
+ npm install -g @openlibing/openlibing-cli
25
+ ```
26
+
27
+ ## 客户端配置
28
+
29
+ ### Trae(手动添加 MCP)
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "openlibing-report": {
35
+ "command": "npx",
36
+ "args": ["-y", "@openlibing/openlibing-cli", "mcp", "start"],
37
+ "env": {
38
+ "OPENLIBING_ENV": "beta",
39
+ "START_MCP_TIMEOUT_MS": "300000"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ > `START_MCP_TIMEOUT_MS` 建议 ≥ 300000(5 分钟),给首次授权留足时间,避免 60 秒默认超时误判失败。
47
+
48
+ ### OpenCode(opencode.jsonc)
49
+
50
+ ```jsonc
51
+ {
52
+ "mcp": {
53
+ "openlibing-report": {
54
+ "type": "local",
55
+ "command": ["npx", "-y", "@openlibing/openlibing-cli", "mcp", "start"],
56
+ "environment": { "OPENLIBING_ENV": "beta" },
57
+ "enabled": true
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## 登录(点一次授权)
64
+
65
+ MCP 连接时若本地无有效凭证,会自动弹出浏览器:
66
+
67
+ 1. **平台选择页** → 点选登录平台(默认 GitCode)
68
+ 2. 跳转该平台授权页 → 点一次"授权"(首次可能需要先登录平台账号)
69
+ 3. 浏览器自动关闭,凭证加密保存,MCP 开始提供工具
70
+
71
+ 命令行方式:
72
+
73
+ ```powershell
74
+ # 自动登录(弹平台选择页,点一次授权)
75
+ npx -y @openlibing/openlibing-cli auth login
76
+
77
+ # 强制手动粘贴 URL 方式(无浏览器/特殊场景)
78
+ npx -y @openlibing/openlibing-cli auth login --manual
79
+
80
+ # 查看登录状态 / 续期 / 退出
81
+ npx -y @openlibing/openlibing-cli auth status
82
+ npx -y @openlibing/openlibing-cli auth refresh
83
+ npx -y @openlibing/openlibing-cli auth logout
84
+ ```
85
+
86
+ > 自动登录依赖本机 Chrome 或 Edge(优先 Chrome,失败回退 Edge)。两者都没有时回退手动粘贴方式。
87
+
88
+ ## 环境变量
89
+
90
+ | 变量 | 默认 | 说明 |
91
+ | --- | --- | --- |
92
+ | `OPENLIBING_ENV` | `beta` | 环境:`beta` / `prod` / `gamma` / `local`,决定网关与 ops 地址 |
93
+ | `OPENLIBING_PLATFORM` | `gitcode` | 默认登录平台(手动模式兜底用;自动模式弹选择页) |
94
+ | `OPENLIBING_GATEWAY_URL` | 按 env | 覆盖网关地址 |
95
+ | `OPENLIBING_OPS_URL` | 按 env | 覆盖 ops 报告接口地址 |
96
+ | `OPENLIBING_PLUGIN_CLIENT` | `agent_arena` | 插件登录 client(须在网关白名单) |
97
+ | `OPENLIBING_PLUGIN_ID` | `openlibing-cli` | 插件标识 |
98
+ | `OPENLIBING_AUTH_FILE` | `~/.openlibing-cli/auth.json` | 覆盖凭证文件路径(测试隔离用) |
99
+
100
+ ## 凭证与安全
101
+
102
+ - 凭证文件:`~/.openlibing-cli/auth.json`(AES-256-GCM,密钥由本机标识派生,换机器/用户不可解密)
103
+ - token / csrf 有效期约 30 分钟:`mcp start` 启动和 401 时自动用 refresh token 续期,无需手动干预
104
+ - refresh token 失效后需重新授权(自动弹浏览器,点一次即可)
105
+ - 401 错误信息明确指引运行 `auth login` / `auth refresh`,不会引导获取网关 cookie
106
+
107
+ ## 发布与维护
108
+
109
+ - 版本发布走项目级 `npm-manage` skill(`publish.py` 解密 npm token 发布,token 不落明文)
110
+ - 版本:`0.1.8`(登录平台选择 / 连接即授权 / 401 自愈 / Playwright 自动登录 / 登录态持久化 / 字段命名同步 workflow 与 job 级时间)
111
+ - 包名:`@openlibing/openlibing-cli`(原 `@openlibing/mcp-cli` 已 deprecated,迁移说明见下)
112
+
113
+ ### 迁移说明(mcp-cli → openlibing-cli)
114
+
115
+ - npm 包:`@openlibing/mcp-cli` → `@openlibing/openlibing-cli`,安装/调用命令同步替换
116
+ - bin 命令:`mcp-cli` → `openlibing-cli`
117
+ - 插件 ID:`openlibing-mcp-cli` → `openlibing-cli`(gateway 插件授权白名单需同步)
118
+ - 凭证目录:`~/.openlibing-mcp-cli/` → `~/.openlibing-cli/`(旧凭证失效,需重新授权一次)
119
+ - 源码仓:`openlibing-mcp-cli` → `openlibing-cli`(git 历史已完整迁移)
120
+
121
+ ## 开发
122
+
123
+ ```powershell
124
+ npm install
125
+ npm run build # tsc 编译到 dist/
126
+ npm test # vitest(握手/凭证/工具注册)
127
+ ```
128
+
129
+ 关键源码:
130
+
131
+ | 模块 | 说明 |
132
+ | --- | --- |
133
+ | `src/auth/browser.ts` | 系统浏览器启动(chrome→edge 回退)、平台选择页、回调捕获 |
134
+ | `src/auth/login.ts` | 插件登录 / 自动授权 / 凭证续期 / MCP 启动鉴权 |
135
+ | `src/auth/store.ts` | 凭证 AES-256-GCM 本地加密存储 |
136
+ | `src/client/reportApi.ts` | 报告接口客户端(401 自动续期 + 引导错误) |
137
+ | `src/mcp/server.ts` | MCP stdio server 组装 |
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 系统浏览器自动化(登录辅助):优先复用本机 Chrome,失败回退 Edge。
3
+ * 用 Playwright 打开授权页并监听跳转,捕获含 code= 的回调 URL,实现"点一次授权即登录"。
4
+ * 不依赖额外下载的浏览器内核(playwright-core 复用系统浏览器)。
5
+ *
6
+ * 登录态持久化:launchPersistentContext 使用专属 profile 目录,平台登录 cookie 落盘
7
+ * 到 ~/.openlibing-cli/browser-profile,首次输密码后,后续重新授权不再要求重新登录平台账号。
8
+ */
9
+ import { type BrowserContext } from "playwright-core";
10
+ /** 启动系统浏览器(持久化 profile + headless=false 弹出窗口),全部失败则抛错 */
11
+ export declare function launchBrowser(): Promise<BrowserContext>;
12
+ /**
13
+ * 打开授权 URL,等待浏览器跳转到含 code= 参数的回调 URL(授权完成),返回完整回调 URL。
14
+ * 用户完成授权前会一直等待,超时抛错。
15
+ * 注意:输出走 stderr(console.error),MCP stdio 场景 stdout 被协议占用。
16
+ */
17
+ export declare function waitForCallbackUrl(authUrl: string, timeoutMs?: number): Promise<string>;
18
+ /** 可登录的三方平台与展示名(顺序即展示顺序) */
19
+ export declare const PLATFORMS: {
20
+ id: string;
21
+ label: string;
22
+ }[];
23
+ /** 生成平台选择页 HTML:用户点选平台后跳转对应授权 URL */
24
+ export declare function buildPlatformPickerHtml(buildAuthUrl: (platform: string) => string): string;
25
+ /**
26
+ * 打开"选择登录平台"页 → 用户点选平台 → 自动跳转该平台授权 → 捕获含 code= 的回调 URL。
27
+ * 供 MCP 启动/工具调用登录使用(stdout 被 stdio 协议占用,提示走 stderr)。
28
+ */
29
+ export declare function waitForCallbackUrlWithPicker(buildAuthUrl: (platform: string) => string, timeoutMs?: number): Promise<string>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 系统浏览器自动化(登录辅助):优先复用本机 Chrome,失败回退 Edge。
3
+ * 用 Playwright 打开授权页并监听跳转,捕获含 code= 的回调 URL,实现"点一次授权即登录"。
4
+ * 不依赖额外下载的浏览器内核(playwright-core 复用系统浏览器)。
5
+ *
6
+ * 登录态持久化:launchPersistentContext 使用专属 profile 目录,平台登录 cookie 落盘
7
+ * 到 ~/.openlibing-cli/browser-profile,首次输密码后,后续重新授权不再要求重新登录平台账号。
8
+ */
9
+ import { chromium } from "playwright-core";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ /** 优先尝试的系统浏览器 channel(按使用率排序) */
13
+ const CHANNELS = ["chrome", "msedge"];
14
+ /** 专属浏览器 profile 目录(登录态持久化,可经 OPENLIBING_BROWSER_PROFILE 覆盖,测试隔离用) */
15
+ const PROFILE_DIR = process.env.OPENLIBING_BROWSER_PROFILE ??
16
+ join(homedir(), ".openlibing-cli", "browser-profile");
17
+ /** 启动系统浏览器(持久化 profile + headless=false 弹出窗口),全部失败则抛错 */
18
+ export async function launchBrowser() {
19
+ let lastErr;
20
+ for (const channel of CHANNELS) {
21
+ try {
22
+ return await chromium.launchPersistentContext(PROFILE_DIR, {
23
+ channel,
24
+ headless: false,
25
+ });
26
+ }
27
+ catch (err) {
28
+ lastErr = err;
29
+ console.error(`未检测到浏览器 ${channel},尝试下一个...`);
30
+ }
31
+ }
32
+ throw new Error(`未找到可用的系统浏览器(Chrome/Edge):${lastErr?.message ?? "unknown"}`);
33
+ }
34
+ /**
35
+ * 打开授权 URL,等待浏览器跳转到含 code= 参数的回调 URL(授权完成),返回完整回调 URL。
36
+ * 用户完成授权前会一直等待,超时抛错。
37
+ * 注意:输出走 stderr(console.error),MCP stdio 场景 stdout 被协议占用。
38
+ */
39
+ export async function waitForCallbackUrl(authUrl, timeoutMs = 5 * 60_000) {
40
+ const browser = await launchBrowser();
41
+ try {
42
+ const page = await browser.newPage();
43
+ console.error("已打开浏览器,请在窗口中完成授权...");
44
+ await page.goto(authUrl);
45
+ await page.waitForURL((url) => url.searchParams.has("code"), { timeout: timeoutMs });
46
+ return page.url();
47
+ }
48
+ finally {
49
+ await browser.close();
50
+ }
51
+ }
52
+ /** 可登录的三方平台与展示名(顺序即展示顺序) */
53
+ export const PLATFORMS = [
54
+ { id: "gitcode", label: "GitCode" },
55
+ { id: "gitee", label: "Gitee(码云)" },
56
+ { id: "github", label: "GitHub" },
57
+ { id: "uniportal", label: "UniPortal" },
58
+ { id: "openubmc", label: "OpenUBMC" },
59
+ ];
60
+ /** 生成平台选择页 HTML:用户点选平台后跳转对应授权 URL */
61
+ export function buildPlatformPickerHtml(buildAuthUrl) {
62
+ const urls = {};
63
+ for (const p of PLATFORMS) {
64
+ urls[p.id] = buildAuthUrl(p.id);
65
+ }
66
+ const urlsJson = JSON.stringify(urls);
67
+ return `<!DOCTYPE html>
68
+ <html lang="zh-CN">
69
+ <head>
70
+ <meta charset="utf-8" />
71
+ <title>OpenLibing 登录</title>
72
+ <style>
73
+ body { font-family: system-ui, sans-serif; display: flex; flex-direction: column;
74
+ align-items: center; justify-content: center; height: 100vh; margin: 0;
75
+ background: #f5f6f8; color: #1f2329; }
76
+ .card { background: #fff; border-radius: 12px; padding: 40px 48px;
77
+ box-shadow: 0 2px 12px rgba(0,0,0,.08); text-align: center; }
78
+ h2 { margin: 0 0 8px; font-size: 20px; }
79
+ p { margin: 0 0 24px; color: #646a73; font-size: 14px; }
80
+ button { display: block; width: 260px; margin: 10px auto; padding: 12px;
81
+ font-size: 15px; border: 1px solid #dcdfe6; border-radius: 8px;
82
+ background: #fff; cursor: pointer; }
83
+ button:hover { border-color: #3370ff; color: #3370ff; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="card">
88
+ <h2>选择登录平台</h2>
89
+ <p>OpenLibing MCP 支持以下平台登录,请选择其一</p>
90
+ <div id="btns"></div>
91
+ </div>
92
+ <script>
93
+ const urls = ${urlsJson};
94
+ const labels = ${JSON.stringify(Object.fromEntries(PLATFORMS.map((p) => [p.id, p.label])))};
95
+ const wrap = document.getElementById('btns');
96
+ for (const id of Object.keys(urls)) {
97
+ const b = document.createElement('button');
98
+ b.textContent = labels[id] || id;
99
+ b.onclick = () => { window.location.href = urls[id]; };
100
+ wrap.appendChild(b);
101
+ }
102
+ </script>
103
+ </body>
104
+ </html>`;
105
+ }
106
+ /**
107
+ * 打开"选择登录平台"页 → 用户点选平台 → 自动跳转该平台授权 → 捕获含 code= 的回调 URL。
108
+ * 供 MCP 启动/工具调用登录使用(stdout 被 stdio 协议占用,提示走 stderr)。
109
+ */
110
+ export async function waitForCallbackUrlWithPicker(buildAuthUrl, timeoutMs = 5 * 60_000) {
111
+ const browser = await launchBrowser();
112
+ try {
113
+ const page = await browser.newPage();
114
+ await page.setContent(buildPlatformPickerHtml(buildAuthUrl));
115
+ console.error("已打开浏览器,请选择登录平台并完成授权...");
116
+ await page.waitForURL((url) => url.searchParams.has("code"), { timeout: timeoutMs });
117
+ return page.url();
118
+ }
119
+ finally {
120
+ await browser.close();
121
+ }
122
+ }
123
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/auth/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { fingerprint, type AuthCredentials } from "./store.js";
2
+ /** 从用户粘贴的 URL 解析一次性 code */
3
+ export declare function parseCodeFromUrl(url: string): string | null;
4
+ /** 生成指定平台的插件授权发起 URL(platform 缺省用 env.platform) */
5
+ export declare function buildAuthUrl(platform?: string): string;
6
+ /** 用一次性 code 换 token + refresh */
7
+ export declare function exchangeCode(code: string): Promise<AuthCredentials>;
8
+ /**
9
+ * 通过 check-account-bind 获取 CSRF token(走网关会话初始化,SPA 同款)。
10
+ * 该接口在网关 csrf.trust.list 内,无 csrf 即可调用;响应头返回 Csrf-Token-Open-Li-Bing。
11
+ */
12
+ export declare function fetchCsrf(token: string): Promise<string>;
13
+ /** 交互式登录:优先 Playwright 自动(点一次授权),失败/超时降级手动粘贴 URL */
14
+ export declare function login(manual?: boolean): Promise<AuthCredentials>;
15
+ /**
16
+ * 纯自动授权(不降级手动粘贴):供 MCP 启动/工具调用场景使用。
17
+ * stdio 被 MCP 协议占用时不能 readLine,手动粘贴会读走协议字节破坏握手。
18
+ * 弹出平台选择页,用户点选平台并完成授权。
19
+ */
20
+ export declare function loginAuto(): Promise<AuthCredentials>;
21
+ /**
22
+ * MCP 连接前确保有可用凭证:
23
+ * 1. 已有凭证 → 自动 refresh 续期(成功即复用,不弹窗);
24
+ * 2. 无凭证或续期失败 → 弹出浏览器完成授权(点一次即完成)。
25
+ * 输出全部走 stderr,避免污染 MCP stdio 协议。
26
+ */
27
+ export declare function ensureAuthenticated(): Promise<void>;
28
+ /** 退出登录:清除本地凭证 */
29
+ export declare function logout(): Promise<void>;
30
+ export { fingerprint };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * 插件登录:复用 gateway 插件凭证链路(LoginController /oauth2/authorization/plugin/{platform} + /plugin/get/access/token)。
3
+ *
4
+ * 流程:
5
+ * 1. 生成 UUID state + 白名单 client,打开浏览器发起插件授权
6
+ * 2. 用户完成三方授权,gateway 回调后 302 到前端页并带一次性 code(存 Redis 10 分钟)
7
+ * 3. 用户把地址栏 URL 粘贴回 CLI,解析 code
8
+ * 4. POST /plugin/get/access/token 换 token + refresh,本地加密存储
9
+ */
10
+ import { randomUUID } from "node:crypto";
11
+ import { exec } from "node:child_process";
12
+ import { createInterface } from "node:readline";
13
+ import { env } from "../config/env.js";
14
+ import { request } from "../client/http.js";
15
+ import { refreshToken } from "./refresh.js";
16
+ import { clearAuth, fingerprint, loadAuth, saveAuth } from "./store.js";
17
+ import { waitForCallbackUrlWithPicker } from "./browser.js";
18
+ /** 打开系统浏览器 */
19
+ function openBrowser(url) {
20
+ const platform = process.platform;
21
+ const cmd = platform === "win32"
22
+ ? `start "" "${url}"`
23
+ : platform === "darwin"
24
+ ? `open "${url}"`
25
+ : `xdg-open "${url}"`;
26
+ exec(cmd, () => undefined);
27
+ }
28
+ /** 从用户粘贴的 URL 解析一次性 code */
29
+ export function parseCodeFromUrl(url) {
30
+ try {
31
+ const u = new URL(url);
32
+ const code = u.searchParams.get("code");
33
+ return code && code.length > 0 ? code : null;
34
+ }
35
+ catch {
36
+ const m = url.match(/[?&]code=([^&]+)/);
37
+ return m ? m[1] : null;
38
+ }
39
+ }
40
+ /** 生成指定平台的插件授权发起 URL(platform 缺省用 env.platform) */
41
+ export function buildAuthUrl(platform = env.platform) {
42
+ const state = randomUUID();
43
+ const params = new URLSearchParams({
44
+ client: env.pluginClient,
45
+ state,
46
+ pluginId: env.pluginId,
47
+ });
48
+ return `${env.gatewayBaseUrl}/oauth2/authorization/plugin/${platform}?${params.toString()}`;
49
+ }
50
+ /** 用一次性 code 换 token + refresh */
51
+ export async function exchangeCode(code) {
52
+ const result = await request(`${env.gatewayBaseUrl}/plugin/get/access/token`, { method: "POST", body: { code } });
53
+ const data = result.data;
54
+ if (result.code !== 200 || !data?.token) {
55
+ throw new Error(`换 token 失败: ${result.message ?? "code 无效或已过期"}`);
56
+ }
57
+ return {
58
+ token: data.token,
59
+ refreshedToken: data.refreshedToken,
60
+ accountName: data.accountName,
61
+ accountId: data.accountId,
62
+ email: data.email,
63
+ updatedAt: new Date().toISOString(),
64
+ };
65
+ }
66
+ const BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
67
+ /**
68
+ * 通过 check-account-bind 获取 CSRF token(走网关会话初始化,SPA 同款)。
69
+ * 该接口在网关 csrf.trust.list 内,无 csrf 即可调用;响应头返回 Csrf-Token-Open-Li-Bing。
70
+ */
71
+ export async function fetchCsrf(token) {
72
+ const origin = new URL(env.gatewayBaseUrl).origin;
73
+ const resp = await fetch(`${env.gatewayBaseUrl}/user/check-account-bind`, {
74
+ method: "GET",
75
+ headers: {
76
+ Cookie: `token=${token}`,
77
+ "User-Agent": BROWSER_UA,
78
+ Accept: "application/json, text/plain, */*",
79
+ Origin: origin,
80
+ Referer: `${origin}/`,
81
+ },
82
+ });
83
+ if (!resp.ok) {
84
+ throw new Error(`获取 csrf 失败: HTTP ${resp.status}`);
85
+ }
86
+ const csrf = resp.headers.get("Csrf-Token-Open-Li-Bing");
87
+ if (!csrf) {
88
+ throw new Error("check-account-bind 响应未返回 Csrf-Token-Open-Li-Bing");
89
+ }
90
+ return csrf;
91
+ }
92
+ function readLine(question) {
93
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
94
+ return new Promise((resolve) => {
95
+ rl.question(question, (answer) => {
96
+ rl.close();
97
+ resolve(answer.trim());
98
+ });
99
+ });
100
+ }
101
+ /** 交互式登录:优先 Playwright 自动(点一次授权),失败/超时降级手动粘贴 URL */
102
+ export async function login(manual = false) {
103
+ const url = buildAuthUrl();
104
+ // 自动模式:Playwright 打开平台选择页(chrome→edge),点选平台后捕获回调 code
105
+ if (!manual) {
106
+ try {
107
+ const callbackUrl = await waitForCallbackUrlWithPicker((platform) => buildAuthUrl(platform));
108
+ const code = parseCodeFromUrl(callbackUrl);
109
+ if (code) {
110
+ return await finishLogin(code);
111
+ }
112
+ }
113
+ catch (err) {
114
+ console.log(`自动登录未完成(${err.message}),切换为手动粘贴方式...`);
115
+ }
116
+ }
117
+ // 手动兜底:打开系统默认浏览器(缺省平台 env.platform),用户粘贴地址栏 URL
118
+ openBrowser(buildAuthUrl());
119
+ console.log(`已打开浏览器发起插件授权(环境 ${env.env},client=${env.pluginClient})`);
120
+ console.log(`若浏览器未自动打开,请手动访问:\n ${url}\n`);
121
+ const pasted = await readLine("请完成授权后,把浏览器地址栏 URL 粘贴到这里(含 code= 参数):");
122
+ const code = parseCodeFromUrl(pasted);
123
+ if (!code) {
124
+ throw new Error("未在 URL 中找到 code 参数,请确认授权流程完整");
125
+ }
126
+ return await finishLogin(code);
127
+ }
128
+ /** 用 code 换 token + csrf + 本地存储 */
129
+ async function finishLogin(code) {
130
+ const auth = await exchangeCode(code);
131
+ // 走会话初始化获取 CSRF(与浏览器 SPA 同款),使业务接口调用通过标准 CSRF 校验
132
+ try {
133
+ auth.csrf = await fetchCsrf(auth.token);
134
+ }
135
+ catch (err) {
136
+ console.error(`提示:获取 csrf 失败(${err.message}),业务接口调用可能被 CSRF 拦截`);
137
+ }
138
+ saveAuth(auth);
139
+ return auth;
140
+ }
141
+ /**
142
+ * 纯自动授权(不降级手动粘贴):供 MCP 启动/工具调用场景使用。
143
+ * stdio 被 MCP 协议占用时不能 readLine,手动粘贴会读走协议字节破坏握手。
144
+ * 弹出平台选择页,用户点选平台并完成授权。
145
+ */
146
+ export async function loginAuto() {
147
+ const callbackUrl = await waitForCallbackUrlWithPicker((platform) => buildAuthUrl(platform));
148
+ const code = parseCodeFromUrl(callbackUrl);
149
+ if (!code) {
150
+ throw new Error("授权回调 URL 中未找到 code 参数");
151
+ }
152
+ return await finishLogin(code);
153
+ }
154
+ /**
155
+ * MCP 连接前确保有可用凭证:
156
+ * 1. 已有凭证 → 自动 refresh 续期(成功即复用,不弹窗);
157
+ * 2. 无凭证或续期失败 → 弹出浏览器完成授权(点一次即完成)。
158
+ * 输出全部走 stderr,避免污染 MCP stdio 协议。
159
+ */
160
+ export async function ensureAuthenticated() {
161
+ const auth = loadAuth();
162
+ if (auth?.refreshedToken) {
163
+ try {
164
+ await refreshToken();
165
+ console.error("[mcp] 凭证已续期");
166
+ return;
167
+ }
168
+ catch (err) {
169
+ console.error(`[mcp] 凭证续期失败(${err.message}),需要重新授权`);
170
+ }
171
+ }
172
+ const result = await loginAuto();
173
+ console.error(`[mcp] 授权完成:${result.accountName ?? "unknown"}`);
174
+ }
175
+ /** 退出登录:清除本地凭证 */
176
+ export async function logout() {
177
+ clearAuth();
178
+ }
179
+ export { fingerprint };
180
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAwB,MAAM,YAAY,CAAC;AAC9F,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAgB5D,cAAc;AACd,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,MAAM,GAAG,GACP,QAAQ,KAAK,OAAO;QAClB,CAAC,CAAC,aAAa,GAAG,GAAG;QACrB,CAAC,CAAC,QAAQ,KAAK,QAAQ;YACrB,CAAC,CAAC,SAAS,GAAG,GAAG;YACjB,CAAC,CAAC,aAAa,GAAG,GAAG,CAAC;IAC5B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AAC7B,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACxC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzB,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,YAAY,CAAC,QAAQ,GAAW,GAAG,CAAC,QAAQ;IAC1D,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,MAAM,EAAE,GAAG,CAAC,YAAY;QACxB,KAAK;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAC,CAAC;IACH,OAAO,GAAG,GAAG,CAAC,cAAc,gCAAgC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC9F,CAAC;AAED,kCAAkC;AAClC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,MAAM,MAAM,GAAG,MAAM,OAAO,CAC1B,GAAG,GAAG,CAAC,cAAc,0BAA0B,EAC/C,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACnC,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,eAAe,MAAM,CAAC,OAAO,IAAI,aAAa,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GACd,iHAAiH,CAAC;AAEpH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAa;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC;IAClD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,cAAc,0BAA0B,EAAE;QACxE,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,SAAS,KAAK,EAAE;YACxB,YAAY,EAAE,UAAU;YACxB,MAAM,EAAE,mCAAmC;YAC3C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,MAAM,GAAG;SACtB;KACF,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACzD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,QAAgB;IAChC,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;YAC/B,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAM,GAAG,KAAK;IACxC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAE3B,sDAAsD;IACtD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,4BAA4B,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC7F,MAAM,IAAI,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAC3C,IAAI,IAAI,EAAE,CAAC;gBACT,OAAO,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,GAAG,CACT,WAAY,GAAa,CAAC,OAAO,gBAAgB,CAClD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,gDAAgD;IAChD,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC;IAC5B,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,CAAC,GAAG,WAAW,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,uCAAuC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,mCAAmC;AACnC,KAAK,UAAU,WAAW,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,iDAAiD;IACjD,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,iBAAkB,GAAa,CAAC,OAAO,qBAAqB,CAAC,CAAC;IAC9E,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,CAAC;IACf,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,WAAW,GAAG,MAAM,4BAA4B,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7F,MAAM,IAAI,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,IAAI,IAAI,EAAE,cAAc,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,YAAY,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,gBAAiB,GAAa,CAAC,OAAO,UAAU,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,cAAc,MAAM,CAAC,WAAW,IAAI,SAAS,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,kBAAkB;AAClB,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,SAAS,EAAE,CAAC;AACd,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type AuthCredentials } from "./store.js";
2
+ /** 用 refresh token 换新 access token;成功后更新本地凭证 */
3
+ export declare function refreshToken(): Promise<AuthCredentials>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 凭证续期:refresh token 换新 access(复用 gateway /plugin/get/access/token/by/refresh/token)。
3
+ * token 过期时由调用方触发,自动刷新后更新本地存储。
4
+ */
5
+ import { env } from "../config/env.js";
6
+ import { request } from "../client/http.js";
7
+ import { fetchCsrf } from "./login.js";
8
+ import { loadAuth, saveAuth } from "./store.js";
9
+ /** 用 refresh token 换新 access token;成功后更新本地凭证 */
10
+ export async function refreshToken() {
11
+ const current = loadAuth();
12
+ if (!current?.refreshedToken) {
13
+ throw new Error("未登录或缺少 refresh token,请先执行 auth login");
14
+ }
15
+ const result = await request(`${env.gatewayBaseUrl}/plugin/get/access/token/by/refresh/token`, { method: "POST", body: { refreshedToken: current.refreshedToken } });
16
+ const data = result.data;
17
+ if (result.code !== 200 || !data?.token) {
18
+ throw new Error(`续期失败: ${result.message ?? "refresh token 无效"}`);
19
+ }
20
+ const updated = {
21
+ ...current,
22
+ token: data.token,
23
+ refreshedToken: data.refreshedToken ?? current.refreshedToken,
24
+ updatedAt: new Date().toISOString(),
25
+ };
26
+ // csrf 与 token 同为 30 分钟有效期,续期后同步刷新(失败不阻塞,业务调用时会再次触发续期)
27
+ try {
28
+ updated.csrf = await fetchCsrf(updated.token);
29
+ }
30
+ catch {
31
+ // 忽略:csrf 刷新失败仅影响 CSRF 校验,可后续重试
32
+ }
33
+ saveAuth(updated);
34
+ return updated;
35
+ }
36
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAwB,MAAM,YAAY,CAAC;AAetE,gDAAgD;AAChD,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,OAAO,CAC1B,GAAG,GAAG,CAAC,cAAc,2CAA2C,EAChE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,CACrE,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,SAAS,MAAM,CAAC,OAAO,IAAI,kBAAkB,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,OAAO,GAAoB;QAC/B,GAAG,OAAO;QACV,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,OAAO,CAAC,cAAc;QAC7D,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,uDAAuD;IACvD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;IACD,QAAQ,CAAC,OAAO,CAAC,CAAC;IAClB,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface AuthCredentials {
2
+ /** 网关插件 JWT token */
3
+ token: string;
4
+ /** 刷新 token(换新 access 用) */
5
+ refreshedToken: string;
6
+ /** CSRF token(调业务接口用,30 分钟有效,随 token 一起管理) */
7
+ csrf?: string;
8
+ accountName?: string;
9
+ accountId?: string;
10
+ email?: string;
11
+ /** 本地保存时间 ISO */
12
+ updatedAt: string;
13
+ }
14
+ export declare function authFilePath(): string;
15
+ export declare function loadAuth(): AuthCredentials | null;
16
+ export declare function saveAuth(auth: AuthCredentials): void;
17
+ export declare function clearAuth(): void;
18
+ /** 稳定指纹,用于 UI 展示脱敏(如 accountId 末 4 位) */
19
+ export declare function fingerprint(accountId?: string): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 凭证本地安全存储:token/refresh 加密落盘(AES-256-GCM),密钥从本机标识派生。
3
+ * 安全责任在用户侧(方案 D):CLI 凭证仅存用户本机,不落服务端。
4
+ */
5
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from "node:crypto";
6
+ import { homedir, hostname } from "node:os";
7
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ const CONFIG_DIR = join(homedir(), ".openlibing-cli");
10
+ /** 凭证文件路径:测试可用 OPENLIBING_AUTH_FILE 覆盖,避免污染真实凭证 */
11
+ const AUTH_FILE = process.env.OPENLIBING_AUTH_FILE ?? join(CONFIG_DIR, "auth.json");
12
+ const VERSION = "v1";
13
+ const SALT = "openlibing-cli:auth:v1";
14
+ /** 从本机标识派生加密密钥(换机器/用户后无法解密,符合本地凭证语义) */
15
+ function deriveKey() {
16
+ const identity = `${hostname()}:${process.env.USERNAME ?? process.env.USER ?? "unknown"}`;
17
+ return scryptSync(identity, SALT, 32);
18
+ }
19
+ function encrypt(plain) {
20
+ const key = deriveKey();
21
+ const iv = randomBytes(12);
22
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
23
+ const enc = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
24
+ const tag = cipher.getAuthTag();
25
+ return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
26
+ }
27
+ function decrypt(payload) {
28
+ const [version, ivB64, tagB64, dataB64] = payload.split(":");
29
+ if (version !== VERSION)
30
+ throw new Error("unsupported credential format");
31
+ const key = deriveKey();
32
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivB64, "base64"));
33
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
34
+ return Buffer.concat([
35
+ decipher.update(Buffer.from(dataB64, "base64")),
36
+ decipher.final(),
37
+ ]).toString("utf8");
38
+ }
39
+ export function authFilePath() {
40
+ return AUTH_FILE;
41
+ }
42
+ export function loadAuth() {
43
+ try {
44
+ const raw = readFileSync(AUTH_FILE, "utf8").trim();
45
+ const parsed = JSON.parse(raw);
46
+ return JSON.parse(decrypt(parsed.encrypted));
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ export function saveAuth(auth) {
53
+ mkdirSync(CONFIG_DIR, { recursive: true });
54
+ const encrypted = encrypt(JSON.stringify(auth));
55
+ writeFileSync(AUTH_FILE, JSON.stringify({ encrypted }, null, 2), { mode: 0o600 });
56
+ }
57
+ export function clearAuth() {
58
+ try {
59
+ rmSync(AUTH_FILE, { force: true });
60
+ }
61
+ catch {
62
+ // 文件不存在即视为已清除
63
+ }
64
+ }
65
+ /** 稳定指纹,用于 UI 展示脱敏(如 accountId 末 4 位) */
66
+ export function fingerprint(accountId) {
67
+ if (!accountId)
68
+ return "unknown";
69
+ return createHash("sha256").update(accountId).digest("hex").slice(0, 8);
70
+ }
71
+ //# sourceMappingURL=store.js.map