@camscanner/mcp-language-server 1.0.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.
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * E2E test for SSO authentication flow.
5
+ *
6
+ * Tests:
7
+ * 1. Credential save / load / clear / expiry
8
+ * 2. Successful SSO callback → credentials saved
9
+ * 3. Missing token → 400 error
10
+ * 4. Unknown path → 404
11
+ * 5. CSRF token extraction from operate server
12
+ *
13
+ * Usage: node test-auth-e2e.js
14
+ */
15
+
16
+ import { describe, it, before, after } from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import http from "node:http";
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+
25
+ // --- Test config ---
26
+ const TEST_PORT = 19877;
27
+ const TEST_CREDENTIALS_DIR = path.join(__dirname, ".test-credentials");
28
+ const TEST_CREDENTIALS_FILE = path.join(TEST_CREDENTIALS_DIR, "credentials.json");
29
+ const FAKE_OPERATE_PORT = 19878;
30
+
31
+ // --- Credential helpers (same logic as production code) ---
32
+
33
+ function saveCredentials(creds) {
34
+ if (!fs.existsSync(TEST_CREDENTIALS_DIR)) {
35
+ fs.mkdirSync(TEST_CREDENTIALS_DIR, { recursive: true });
36
+ }
37
+ fs.writeFileSync(TEST_CREDENTIALS_FILE, JSON.stringify(creds, null, 2));
38
+ }
39
+
40
+ function loadCredentials() {
41
+ try {
42
+ if (!fs.existsSync(TEST_CREDENTIALS_FILE)) return null;
43
+ const data = JSON.parse(fs.readFileSync(TEST_CREDENTIALS_FILE, "utf-8"));
44
+ if (data.expiresAt && Date.now() < data.expiresAt) return data;
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function clearCredentials() {
52
+ try {
53
+ if (fs.existsSync(TEST_CREDENTIALS_FILE)) fs.unlinkSync(TEST_CREDENTIALS_FILE);
54
+ } catch {}
55
+ }
56
+
57
+ function cleanup() {
58
+ try {
59
+ if (fs.existsSync(TEST_CREDENTIALS_FILE)) fs.unlinkSync(TEST_CREDENTIALS_FILE);
60
+ if (fs.existsSync(TEST_CREDENTIALS_DIR)) fs.rmdirSync(TEST_CREDENTIALS_DIR);
61
+ } catch {}
62
+ }
63
+
64
+ /** HTTP GET → { statusCode, headers, body } */
65
+ function httpGet(url, timeoutMs = 5000) {
66
+ return new Promise((resolve, reject) => {
67
+ const req = http.get(url, { headers: { Connection: "close" } }, (res) => {
68
+ let body = "";
69
+ res.on("data", (chunk) => (body += chunk));
70
+ res.on("end", () =>
71
+ resolve({ statusCode: res.statusCode, headers: res.headers, body })
72
+ );
73
+ });
74
+ req.on("error", reject);
75
+ req.setTimeout(timeoutMs, () => { req.destroy(new Error("httpGet timeout")); });
76
+ });
77
+ }
78
+
79
+ // --- Fake operate server (returns _csrf cookie) ---
80
+
81
+ let fakeOperateServer;
82
+ function startFakeOperateServer() {
83
+ return new Promise((resolve) => {
84
+ fakeOperateServer = http.createServer((req, res) => {
85
+ if (req.url.startsWith("/site/get-config")) {
86
+ res.writeHead(200, {
87
+ "Content-Type": "application/json",
88
+ "Set-Cookie": "_csrf=test_csrf_token_abc123; Path=/",
89
+ });
90
+ res.end(JSON.stringify({ code: 0 }));
91
+ } else {
92
+ res.writeHead(404);
93
+ res.end("Not found");
94
+ }
95
+ });
96
+ fakeOperateServer.listen(FAKE_OPERATE_PORT, () => resolve());
97
+ });
98
+ }
99
+
100
+ // --- Auth callback server (mimics production logic, no auto-close) ---
101
+
102
+ function createAuthServer(baseUrl) {
103
+ let resolveAuth, rejectAuth;
104
+ const authPromise = new Promise((res, rej) => {
105
+ resolveAuth = res;
106
+ rejectAuth = rej;
107
+ });
108
+
109
+ const server = http.createServer(async (req, res) => {
110
+ const reqUrl = new URL(req.url || "/", `http://localhost:${TEST_PORT}`);
111
+
112
+ if (reqUrl.pathname === "/callback") {
113
+ const ssoToken = reqUrl.searchParams.get("token");
114
+ if (!ssoToken) {
115
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
116
+ res.end("<h2>登录失败:未收到 token</h2>", () => {
117
+ rejectAuth(new Error("No token received from SSO"));
118
+ });
119
+ return;
120
+ }
121
+
122
+ try {
123
+ const csrfRes = await httpGet(`${baseUrl}/site/get-config`);
124
+ const cookies = csrfRes.headers["set-cookie"] || [];
125
+ let csrf = "";
126
+ for (const c of cookies) {
127
+ const m = c.match(/^_csrf=([^;]*)/);
128
+ if (m) { csrf = m[1]; break; }
129
+ }
130
+
131
+ const creds = {
132
+ ssoToken,
133
+ csrfToken: csrf,
134
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000,
135
+ };
136
+ saveCredentials(creds);
137
+
138
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
139
+ res.end(
140
+ `<html><body><h1>登录成功</h1><p>正在返回应用…</p></body></html>` +
141
+ `<script>setTimeout(function(){ window.close(); }, 1000);</script>`
142
+ );
143
+ resolveAuth(creds);
144
+ } catch (err) {
145
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
146
+ res.end(`<h2>登录失败:${err.message}</h2>`);
147
+ rejectAuth(err);
148
+ }
149
+ } else {
150
+ res.writeHead(404);
151
+ res.end("Not found");
152
+ }
153
+ });
154
+
155
+ return { server, authPromise };
156
+ }
157
+
158
+ function listenServer(server) {
159
+ return new Promise((resolve) => server.listen(TEST_PORT, resolve));
160
+ }
161
+
162
+ function closeServer(server) {
163
+ return new Promise((resolve) => server.close(() => resolve()));
164
+ }
165
+
166
+ // --- Tests ---
167
+
168
+ describe("SSO Auth E2E", () => {
169
+ before(async () => {
170
+ cleanup();
171
+ await startFakeOperateServer();
172
+ });
173
+
174
+ after(async () => {
175
+ cleanup();
176
+ if (fakeOperateServer) await new Promise((r) => fakeOperateServer.close(r));
177
+ });
178
+
179
+ describe("Credential management", () => {
180
+ it("saveCredentials + loadCredentials round-trip", () => {
181
+ const creds = { ssoToken: "tk_1", csrfToken: "cs_1", expiresAt: Date.now() + 60000 };
182
+ saveCredentials(creds);
183
+ assert.deepStrictEqual(loadCredentials(), creds);
184
+ });
185
+
186
+ it("expired credentials return null", () => {
187
+ saveCredentials({ ssoToken: "tk_exp", csrfToken: "cs_exp", expiresAt: Date.now() - 1000 });
188
+ assert.strictEqual(loadCredentials(), null);
189
+ });
190
+
191
+ it("clearCredentials removes file", () => {
192
+ saveCredentials({ ssoToken: "tk_del", csrfToken: "cs_del", expiresAt: Date.now() + 60000 });
193
+ assert.ok(fs.existsSync(TEST_CREDENTIALS_FILE));
194
+ clearCredentials();
195
+ assert.ok(!fs.existsSync(TEST_CREDENTIALS_FILE));
196
+ });
197
+
198
+ it("loadCredentials returns null when no file", () => {
199
+ clearCredentials();
200
+ assert.strictEqual(loadCredentials(), null);
201
+ });
202
+ });
203
+
204
+ describe("Auth callback server", () => {
205
+ it("successful login: returns 200, saves credentials, includes window.close()", async () => {
206
+ const baseUrl = `http://localhost:${FAKE_OPERATE_PORT}`;
207
+ const { server, authPromise } = createAuthServer(baseUrl);
208
+ await listenServer(server);
209
+
210
+ try {
211
+ const res = await httpGet(`http://localhost:${TEST_PORT}/callback?token=sso_test_789`);
212
+
213
+ assert.strictEqual(res.statusCode, 200);
214
+ assert.ok(res.body.includes("登录成功"));
215
+ assert.ok(res.body.includes("window.close()"));
216
+
217
+ const creds = await authPromise;
218
+ assert.strictEqual(creds.ssoToken, "sso_test_789");
219
+ assert.strictEqual(creds.csrfToken, "test_csrf_token_abc123");
220
+ assert.ok(creds.expiresAt > Date.now());
221
+
222
+ // Verify persisted
223
+ const saved = loadCredentials();
224
+ assert.deepStrictEqual(saved, creds);
225
+ } finally {
226
+ await closeServer(server);
227
+ }
228
+ });
229
+
230
+ it("missing token: rejects with 'No token received'", async () => {
231
+ const baseUrl = `http://localhost:${FAKE_OPERATE_PORT}`;
232
+ const { server, authPromise } = createAuthServer(baseUrl);
233
+ await listenServer(server);
234
+
235
+ // Run both in parallel — one may resolve/reject before the other
236
+ const [httpSettled, authSettled] = await Promise.allSettled([
237
+ httpGet(`http://localhost:${TEST_PORT}/callback`),
238
+ authPromise,
239
+ ]);
240
+
241
+ // authPromise must reject with the right error
242
+ assert.strictEqual(authSettled.status, "rejected");
243
+ assert.match(authSettled.reason.message, /No token received/);
244
+
245
+ // HTTP response: if fulfilled, should be 400
246
+ if (httpSettled.status === "fulfilled") {
247
+ assert.strictEqual(httpSettled.value.statusCode, 400);
248
+ assert.ok(httpSettled.value.body.includes("登录失败"));
249
+ }
250
+
251
+ await closeServer(server);
252
+ });
253
+
254
+ it("unknown path: returns 404", async () => {
255
+ const baseUrl = `http://localhost:${FAKE_OPERATE_PORT}`;
256
+ const { server } = createAuthServer(baseUrl);
257
+ await listenServer(server);
258
+
259
+ try {
260
+ const res = await httpGet(`http://localhost:${TEST_PORT}/unknown`);
261
+ assert.strictEqual(res.statusCode, 404);
262
+ } finally {
263
+ await closeServer(server);
264
+ }
265
+ });
266
+ });
267
+
268
+ describe("CSRF token extraction", () => {
269
+ it("fake operate server returns _csrf in Set-Cookie", async () => {
270
+ const res = await httpGet(`http://localhost:${FAKE_OPERATE_PORT}/site/get-config`);
271
+ assert.strictEqual(res.statusCode, 200);
272
+ const cookies = res.headers["set-cookie"] || [];
273
+ const csrfCookie = cookies.find((c) => c.startsWith("_csrf="));
274
+ assert.ok(csrfCookie);
275
+ assert.ok(csrfCookie.includes("test_csrf_token_abc123"));
276
+ });
277
+ });
278
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
17
+ }
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # 快速更新 Cookie 和 CSRF Token
3
+ # 用法: bash update-cookie.sh
4
+ # 或: bash update-cookie.sh "从浏览器复制的完整 curl 命令"
5
+
6
+ MCP_SERVER_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+
8
+ echo "=== 更新多语言平台认证信息 ==="
9
+ echo ""
10
+
11
+ # 如果传入了 curl 命令,自动提取 cookie 和 csrf token
12
+ if [ -n "$1" ]; then
13
+ CURL_CMD="$*"
14
+
15
+ # 从 curl 命令中提取 cookie (-b 或 --cookie 或 -H 'cookie: ...')
16
+ COOKIE=$(echo "$CURL_CMD" | grep -oP "(?<=-b ').*?(?=')" 2>/dev/null || \
17
+ echo "$CURL_CMD" | sed -n "s/.*-b '\([^']*\)'.*/\1/p" 2>/dev/null || \
18
+ echo "$CURL_CMD" | sed -n "s/.*-b \"\([^\"]*\)\".*/\1/p" 2>/dev/null)
19
+
20
+ if [ -z "$COOKIE" ]; then
21
+ COOKIE=$(echo "$CURL_CMD" | sed -n "s/.*[Cc]ookie: \([^'\"]*\)['\"].*/\1/p" 2>/dev/null)
22
+ fi
23
+
24
+ # 从 curl 命令中提取 csrf token
25
+ CSRF=$(echo "$CURL_CMD" | sed -n "s/.*[Xx]-[Cc][Ss][Rr][Ff]-[Tt]oken: \([^'\"]*\)['\"].*/\1/p" 2>/dev/null)
26
+
27
+ if [ -n "$COOKIE" ]; then
28
+ echo "$COOKIE" > "$MCP_SERVER_DIR/.cookie"
29
+ echo "Cookie 已更新 (${#COOKIE} 字符)"
30
+ else
31
+ echo "未能从 curl 命令中提取 Cookie,请手动输入"
32
+ fi
33
+
34
+ if [ -n "$CSRF" ]; then
35
+ echo "$CSRF" > "$MCP_SERVER_DIR/.csrf-token"
36
+ echo "CSRF Token 已更新"
37
+ else
38
+ echo "未能从 curl 命令中提取 CSRF Token,请手动输入"
39
+ fi
40
+
41
+ if [ -n "$COOKIE" ] && [ -n "$CSRF" ]; then
42
+ echo ""
43
+ echo "更新完成!请重启 Claude Code 使其生效。"
44
+ exit 0
45
+ fi
46
+ echo ""
47
+ fi
48
+
49
+ # 交互式输入
50
+ echo "获取方式:"
51
+ echo " 1. 浏览器打开 https://operate-test.intsig.net/multilanguage"
52
+ echo " 2. 登录后打开 DevTools → Network"
53
+ echo " 3. 做一次搜索操作,找到 get-string-search 请求"
54
+ echo " 4. 右键 → Copy → Copy as cURL"
55
+ echo ""
56
+
57
+ if [ -z "$COOKIE" ]; then
58
+ read -p "粘贴 Cookie 值: " COOKIE
59
+ if [ -n "$COOKIE" ]; then
60
+ echo "$COOKIE" > "$MCP_SERVER_DIR/.cookie"
61
+ echo "Cookie 已保存 (${#COOKIE} 字符)"
62
+ fi
63
+ fi
64
+
65
+ if [ -z "$CSRF" ]; then
66
+ echo ""
67
+ read -p "粘贴 X-CSRF-Token 值: " CSRF
68
+ if [ -n "$CSRF" ]; then
69
+ echo "$CSRF" > "$MCP_SERVER_DIR/.csrf-token"
70
+ echo "CSRF Token 已保存"
71
+ fi
72
+ fi
73
+
74
+ echo ""
75
+ echo "更新完成!请重启 Claude Code 使其生效。"