@infimind-next/ai-hive-mcp 0.1.1 → 0.2.1

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,214 @@
1
+ import { createServer, } from "node:http";
2
+ export function listenForWorkBuddyCallback(_input) {
3
+ const input = _input;
4
+ return new Promise((resolve, reject) => {
5
+ let claimed = false;
6
+ let settled = false;
7
+ let timeout;
8
+ let closePromise;
9
+ let resolveCompletion;
10
+ let rejectCompletion;
11
+ const completion = new Promise((completionResolve, completionReject) => {
12
+ resolveCompletion = completionResolve;
13
+ rejectCompletion = completionReject;
14
+ });
15
+ void completion.catch(() => undefined);
16
+ const server = createServer((request, response) => {
17
+ void handleRequest(request, response).catch(() => {
18
+ void handleUnexpectedRequestFailure(response).catch(() => {
19
+ response.destroy();
20
+ });
21
+ });
22
+ });
23
+ const clearDeadline = () => {
24
+ if (timeout) {
25
+ clearTimeout(timeout);
26
+ timeout = undefined;
27
+ }
28
+ };
29
+ const closeServer = (force = false) => {
30
+ if (closePromise) {
31
+ return closePromise;
32
+ }
33
+ closePromise = new Promise((closeResolve) => {
34
+ if (!server.listening) {
35
+ closeResolve();
36
+ return;
37
+ }
38
+ server.close(() => closeResolve());
39
+ if (force) {
40
+ server.closeAllConnections();
41
+ }
42
+ else {
43
+ server.closeIdleConnections();
44
+ }
45
+ });
46
+ return closePromise;
47
+ };
48
+ const finishSuccess = async () => {
49
+ if (settled) {
50
+ return;
51
+ }
52
+ settled = true;
53
+ clearDeadline();
54
+ await closeServer();
55
+ resolveCompletion();
56
+ };
57
+ const finishFailure = async (message, force = false) => {
58
+ if (settled) {
59
+ return;
60
+ }
61
+ settled = true;
62
+ claimed = true;
63
+ clearDeadline();
64
+ await closeServer(force);
65
+ rejectCompletion(new Error(message));
66
+ };
67
+ async function handleRequest(request, response) {
68
+ let url;
69
+ try {
70
+ url = new URL(request.url ?? "/", "http://127.0.0.1");
71
+ }
72
+ catch {
73
+ sendHtml(response, 400, failureHtml());
74
+ return;
75
+ }
76
+ if (url.pathname !== "/oauth/callback") {
77
+ sendHtml(response, 404, failureHtml());
78
+ return;
79
+ }
80
+ if (request.method !== "GET") {
81
+ sendHtml(response, 405, failureHtml());
82
+ return;
83
+ }
84
+ const states = url.searchParams.getAll("state");
85
+ const codes = url.searchParams.getAll("code");
86
+ if (states.length !== 1 ||
87
+ codes.length !== 1 ||
88
+ !states[0] ||
89
+ !codes[0] ||
90
+ states[0] !== input.expectedState) {
91
+ sendHtml(response, 400, failureHtml());
92
+ return;
93
+ }
94
+ if (claimed) {
95
+ sendHtml(response, 409, failureHtml());
96
+ return;
97
+ }
98
+ claimed = true;
99
+ clearDeadline();
100
+ try {
101
+ await input.onCode(codes[0]);
102
+ }
103
+ catch {
104
+ if (!settled) {
105
+ await sendHtmlAndWait(response, 500, failureHtml());
106
+ await finishFailure("WorkBuddy 授权失败,请重新连接");
107
+ }
108
+ return;
109
+ }
110
+ if (settled) {
111
+ response.destroy();
112
+ return;
113
+ }
114
+ await sendHtmlAndWait(response, 200, successHtml());
115
+ await finishSuccess();
116
+ }
117
+ async function handleUnexpectedRequestFailure(response) {
118
+ if (!response.headersSent && !response.destroyed) {
119
+ await sendHtmlAndWait(response, 500, failureHtml());
120
+ }
121
+ else {
122
+ response.destroy();
123
+ }
124
+ if (claimed) {
125
+ await finishFailure("WorkBuddy 授权失败,请重新连接", true);
126
+ }
127
+ }
128
+ server.once("error", (error) => {
129
+ clearDeadline();
130
+ if (!server.listening) {
131
+ reject(error);
132
+ return;
133
+ }
134
+ void finishFailure("WorkBuddy 回调监听失败", true);
135
+ });
136
+ server.listen(0, "127.0.0.1", () => {
137
+ const address = server.address();
138
+ if (!address || typeof address === "string") {
139
+ void finishFailure("WorkBuddy 回调监听失败", true);
140
+ reject(new Error("WorkBuddy 回调监听失败"));
141
+ return;
142
+ }
143
+ timeout = setTimeout(() => {
144
+ void finishFailure("WorkBuddy 授权等待超时", true);
145
+ }, input.timeoutMs);
146
+ resolve({
147
+ redirectUri: `http://127.0.0.1:${address.port}/oauth/callback`,
148
+ completion,
149
+ close: async () => {
150
+ if (!settled) {
151
+ settled = true;
152
+ claimed = true;
153
+ clearDeadline();
154
+ await closeServer(true);
155
+ rejectCompletion(new Error("WorkBuddy 回调监听已关闭"));
156
+ return;
157
+ }
158
+ await closeServer(true);
159
+ },
160
+ });
161
+ });
162
+ });
163
+ }
164
+ function sendHtml(response, status, body) {
165
+ response.writeHead(status, {
166
+ "Cache-Control": "no-store",
167
+ Connection: "close",
168
+ "Content-Type": "text/html; charset=utf-8",
169
+ });
170
+ response.end(body);
171
+ }
172
+ function sendHtmlAndWait(response, status, body) {
173
+ return new Promise((resolve) => {
174
+ response.writeHead(status, {
175
+ "Cache-Control": "no-store",
176
+ Connection: "close",
177
+ "Content-Type": "text/html; charset=utf-8",
178
+ });
179
+ response.end(body, resolve);
180
+ });
181
+ }
182
+ function page(title, content) {
183
+ return `<!doctype html>
184
+ <html lang="zh-CN">
185
+ <head>
186
+ <meta charset="utf-8">
187
+ <meta name="viewport" content="width=device-width, initial-scale=1">
188
+ <meta name="referrer" content="no-referrer">
189
+ <title>${title} - AI-HIVE</title>
190
+ <style>
191
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: sans-serif; background: #f8fafc; color: #0f172a; }
192
+ main { width: min(420px, calc(100% - 48px)); padding: 32px; border: 1px solid #e2e8f0; border-radius: 16px; background: white; box-shadow: 0 16px 40px rgb(15 23 42 / 8%); }
193
+ h1 { margin-top: 0; }
194
+ button { border: 0; border-radius: 8px; padding: 10px 16px; background: #2563eb; color: white; cursor: pointer; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <main>${content}</main>
199
+ </body>
200
+ </html>`;
201
+ }
202
+ function successHtml() {
203
+ return page("授权成功", `<h1>授权成功</h1>
204
+ <p>请返回 WorkBuddy 继续使用。</p>
205
+ <p>页面将尝试自动关闭;如果没有关闭,请手动关闭。</p>
206
+ <button type="button" onclick="window.close()">关闭页面</button>
207
+ <script>
208
+ window.setTimeout(() => window.close(), 300);
209
+ </script>`);
210
+ }
211
+ function failureHtml() {
212
+ return page("授权失败", `<h1>授权失败</h1>
213
+ <p>请返回 WorkBuddy 后重新连接。</p>`);
214
+ }
@@ -0,0 +1,31 @@
1
+ export type WorkBuddyTokenPayload = {
2
+ tokenType: "Bearer";
3
+ accessToken: string;
4
+ refreshToken: string;
5
+ expiresIn: 3600;
6
+ refreshExpiresIn: 2592000;
7
+ };
8
+ declare class WorkBuddyOAuthError extends Error {
9
+ }
10
+ export declare function isWorkBuddyOAuthError(error: unknown): error is WorkBuddyOAuthError;
11
+ export declare class WorkBuddyOAuthClient {
12
+ private readonly apiEndpoint;
13
+ private readonly timeoutMs;
14
+ private readonly fetchImpl;
15
+ constructor(input: {
16
+ apiEndpoint: string;
17
+ timeoutMs: number;
18
+ fetchImpl?: typeof fetch;
19
+ });
20
+ exchangeAuthorizationCode(input: {
21
+ code: string;
22
+ redirectUri: string;
23
+ codeVerifier: string;
24
+ }): Promise<WorkBuddyTokenPayload>;
25
+ refreshToken(refreshToken: string): Promise<WorkBuddyTokenPayload>;
26
+ revoke(refreshToken: string): Promise<void>;
27
+ private requestToken;
28
+ private post;
29
+ private withTimeout;
30
+ }
31
+ export {};
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ const tokenPayloadSchema = z
3
+ .object({
4
+ tokenType: z.literal("Bearer"),
5
+ accessToken: z.string().startsWith("wb-at-"),
6
+ refreshToken: z.string().startsWith("wb-rt-"),
7
+ expiresIn: z.literal(3600),
8
+ refreshExpiresIn: z.literal(2592000),
9
+ })
10
+ .strict();
11
+ const revokeResponseSchema = z.object({ ok: z.literal(true) }).strict();
12
+ class WorkBuddyOAuthError extends Error {
13
+ }
14
+ export function isWorkBuddyOAuthError(error) {
15
+ return error instanceof WorkBuddyOAuthError;
16
+ }
17
+ function timeoutError() {
18
+ return new WorkBuddyOAuthError("WorkBuddy 认证请求超时,请稍后重试");
19
+ }
20
+ function requestError() {
21
+ return new WorkBuddyOAuthError("WorkBuddy 认证请求失败,请稍后重试");
22
+ }
23
+ function responseError() {
24
+ return new WorkBuddyOAuthError("WorkBuddy 认证响应无效");
25
+ }
26
+ async function discardResponseBody(response) {
27
+ try {
28
+ await response.body?.cancel();
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ }
34
+ export class WorkBuddyOAuthClient {
35
+ apiEndpoint;
36
+ timeoutMs;
37
+ fetchImpl;
38
+ constructor(input) {
39
+ this.apiEndpoint = input.apiEndpoint.replace(/\/+$/, "");
40
+ this.timeoutMs = input.timeoutMs;
41
+ this.fetchImpl = input.fetchImpl ?? fetch;
42
+ }
43
+ async exchangeAuthorizationCode(input) {
44
+ return this.requestToken({
45
+ grantType: "authorization_code",
46
+ code: input.code,
47
+ redirectUri: input.redirectUri,
48
+ codeVerifier: input.codeVerifier,
49
+ });
50
+ }
51
+ async refreshToken(refreshToken) {
52
+ return this.requestToken({
53
+ grantType: "refresh_token",
54
+ refreshToken,
55
+ });
56
+ }
57
+ async revoke(refreshToken) {
58
+ const body = await this.post("/workbuddy/oauth/revoke", {
59
+ refreshToken,
60
+ });
61
+ if (!revokeResponseSchema.safeParse(body).success) {
62
+ throw responseError();
63
+ }
64
+ }
65
+ async requestToken(body) {
66
+ const responseBody = await this.post("/workbuddy/oauth/token", body);
67
+ const parsed = tokenPayloadSchema.safeParse(responseBody);
68
+ if (!parsed.success) {
69
+ throw responseError();
70
+ }
71
+ return parsed.data;
72
+ }
73
+ async post(path, body) {
74
+ return this.withTimeout(async (signal) => {
75
+ let response;
76
+ try {
77
+ response = await this.fetchImpl(`${this.apiEndpoint}${path}`, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify(body),
81
+ signal,
82
+ });
83
+ }
84
+ catch {
85
+ if (signal.aborted) {
86
+ throw timeoutError();
87
+ }
88
+ throw requestError();
89
+ }
90
+ if (!response.ok) {
91
+ await discardResponseBody(response);
92
+ throw new WorkBuddyOAuthError("WorkBuddy 认证失败,请重新连接");
93
+ }
94
+ try {
95
+ return await response.json();
96
+ }
97
+ catch {
98
+ throw responseError();
99
+ }
100
+ });
101
+ }
102
+ async withTimeout(operation) {
103
+ const controller = new AbortController();
104
+ let timeout;
105
+ const timeoutPromise = new Promise((_, reject) => {
106
+ timeout = setTimeout(() => {
107
+ controller.abort();
108
+ reject(timeoutError());
109
+ }, this.timeoutMs);
110
+ });
111
+ try {
112
+ return await Promise.race([operation(controller.signal), timeoutPromise]);
113
+ }
114
+ catch (error) {
115
+ if (controller.signal.aborted) {
116
+ throw timeoutError();
117
+ }
118
+ throw error;
119
+ }
120
+ finally {
121
+ if (timeout) {
122
+ clearTimeout(timeout);
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,6 @@
1
+ export type WorkBuddyPkcePair = {
2
+ state: string;
3
+ codeVerifier: string;
4
+ codeChallenge: string;
5
+ };
6
+ export declare function createPkcePair(): WorkBuddyPkcePair;
@@ -0,0 +1,11 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export function createPkcePair() {
3
+ const codeVerifier = randomBytes(32).toString("base64url");
4
+ return {
5
+ state: randomBytes(32).toString("base64url"),
6
+ codeVerifier,
7
+ codeChallenge: createHash("sha256")
8
+ .update(codeVerifier)
9
+ .digest("base64url"),
10
+ };
11
+ }
package/package.json CHANGED
@@ -1,28 +1,32 @@
1
1
  {
2
2
  "name": "@infimind-next/ai-hive-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=20.19.0"
8
+ },
6
9
  "bin": {
7
10
  "ai-hive-mcp": "dist/index.js"
8
11
  },
9
12
  "files": [
10
13
  "dist"
11
14
  ],
12
- "scripts": {
13
- "build": "tsc -p tsconfig.json",
14
- "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
15
- "lint": "eslint \"src/**/*.ts\"",
16
- "prepack": "pnpm run build",
17
- "test": "vitest run"
18
- },
19
15
  "dependencies": {
20
16
  "@modelcontextprotocol/sdk": "^1.29.0",
21
- "file-type": "^22.0.1",
17
+ "file-type": "^21.3.4",
22
18
  "zod": "^3.24.0"
23
19
  },
24
20
  "devDependencies": {
21
+ "eslint": "^9.0.0",
25
22
  "typescript": "^5.0.0",
23
+ "typescript-eslint": "^8.0.0",
26
24
  "vitest": "^3.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
29
+ "lint": "eslint \"src/**/*.ts\"",
30
+ "test": "vitest run"
27
31
  }
28
- }
32
+ }