@infimind-next/ai-hive-mcp 0.1.1 → 0.2.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/dist/api-client.d.ts +11 -7
- package/dist/api-client.js +45 -15
- package/dist/auth-token-provider.d.ts +34 -0
- package/dist/auth-token-provider.js +115 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +320 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -7
- package/dist/index.js +15 -5
- package/dist/server.d.ts +11 -2
- package/dist/server.js +35 -8
- package/dist/workbuddy-credentials.d.ts +32 -0
- package/dist/workbuddy-credentials.js +340 -0
- package/dist/workbuddy-loopback.d.ts +10 -0
- package/dist/workbuddy-loopback.js +214 -0
- package/dist/workbuddy-oauth-client.d.ts +31 -0
- package/dist/workbuddy-oauth-client.js +126 -0
- package/dist/workbuddy-pkce.d.ts +6 -0
- package/dist/workbuddy-pkce.js +11 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from "./cli.js";
|
|
2
3
|
import { parseConfig } from "./config.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
function safeTopLevelMessage(error) {
|
|
5
|
+
if (error instanceof Error &&
|
|
6
|
+
["未知命令", "未知选项", "选项缺少值"].includes(error.message)) {
|
|
7
|
+
return error.message;
|
|
8
|
+
}
|
|
9
|
+
return "命令执行失败";
|
|
10
|
+
}
|
|
11
|
+
Promise.resolve()
|
|
12
|
+
.then(() => parseConfig({ argv: process.argv, env: process.env }))
|
|
13
|
+
.then((config) => runCli(config))
|
|
14
|
+
.then((code) => {
|
|
15
|
+
process.exitCode = code;
|
|
16
|
+
}, (error) => {
|
|
17
|
+
console.error(safeTopLevelMessage(error));
|
|
8
18
|
process.exitCode = 1;
|
|
9
19
|
});
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { AiHiveApiClient } from "./api-client.js";
|
|
4
|
+
import { type BearerTokenProvider } from "./auth-token-provider.js";
|
|
5
|
+
import type { McpClientConfig } from "./config.js";
|
|
6
|
+
import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
|
|
7
|
+
export declare const SERVER_VERSION = "0.2.0";
|
|
4
8
|
declare function content(value: unknown): {
|
|
5
9
|
content: {
|
|
6
10
|
type: "text";
|
|
@@ -14,14 +18,19 @@ export type ToolDefinition = {
|
|
|
14
18
|
handler: (args: Record<string, unknown>) => Promise<ReturnType<typeof content>>;
|
|
15
19
|
};
|
|
16
20
|
export declare function createToolDefinitions(client: () => ApiClientLike): ToolDefinition[];
|
|
21
|
+
export type ResolvedServerAuth = {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
tokenProvider: BearerTokenProvider;
|
|
24
|
+
};
|
|
25
|
+
export declare function resolveServerAuth(config: McpClientConfig, store: WorkBuddyCredentialStore): Promise<ResolvedServerAuth>;
|
|
17
26
|
export declare function createServer(input: {
|
|
18
27
|
endpoint: string;
|
|
19
|
-
|
|
28
|
+
tokenProvider: BearerTokenProvider;
|
|
20
29
|
timeoutMs: number;
|
|
21
30
|
}): McpServer;
|
|
22
31
|
export declare function runServer(input: {
|
|
23
32
|
endpoint: string;
|
|
24
|
-
|
|
33
|
+
tokenProvider: BearerTokenProvider;
|
|
25
34
|
timeoutMs: number;
|
|
26
35
|
}): Promise<void>;
|
|
27
36
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -3,15 +3,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, listModelsShape, } from "./schemas.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { AiHiveApiClient } from "./api-client.js";
|
|
6
|
+
import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
|
|
6
7
|
import { uploadMediaFromPath } from "./local-media.js";
|
|
7
|
-
|
|
8
|
+
import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
|
|
9
|
+
export const SERVER_VERSION = "0.2.0";
|
|
8
10
|
const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
|
|
9
|
-
function requireKey(key) {
|
|
10
|
-
if (!key) {
|
|
11
|
-
throw new Error("MCP API Key 未配置,请在配置中传入 --key 或 AI_HIVE_MCP_KEY");
|
|
12
|
-
}
|
|
13
|
-
return key;
|
|
14
|
-
}
|
|
15
11
|
function content(value) {
|
|
16
12
|
return {
|
|
17
13
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
@@ -70,11 +66,42 @@ export function createToolDefinitions(client) {
|
|
|
70
66
|
},
|
|
71
67
|
];
|
|
72
68
|
}
|
|
69
|
+
export async function resolveServerAuth(config, store) {
|
|
70
|
+
if (config.key !== null) {
|
|
71
|
+
if (!config.key) {
|
|
72
|
+
throw new Error("MCP API Key 未配置,请在配置中传入 --key 或 AI_HIVE_MCP_KEY");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
endpoint: config.endpoint,
|
|
76
|
+
tokenProvider: new StaticBearerTokenProvider(config.key),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const credentials = await store.read();
|
|
80
|
+
if (!credentials) {
|
|
81
|
+
throw new Error("尚未连接 AI-HIVE,请在 WorkBuddy 中重新连接");
|
|
82
|
+
}
|
|
83
|
+
if (config.endpointExplicit &&
|
|
84
|
+
normalizeApiEndpoint(config.endpoint) !==
|
|
85
|
+
normalizeApiEndpoint(credentials.apiEndpoint)) {
|
|
86
|
+
throw new Error("API endpoint 与 WorkBuddy 凭证不一致,请在 WorkBuddy 中重新认证");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
endpoint: credentials.apiEndpoint,
|
|
90
|
+
tokenProvider: new WorkBuddyTokenProvider({
|
|
91
|
+
store,
|
|
92
|
+
expectedApiEndpoint: credentials.apiEndpoint,
|
|
93
|
+
oauthClientFor: (apiEndpoint) => new WorkBuddyOAuthClient({
|
|
94
|
+
apiEndpoint,
|
|
95
|
+
timeoutMs: config.timeoutMs,
|
|
96
|
+
}),
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
73
100
|
export function createServer(input) {
|
|
74
101
|
const server = new McpServer({ name: "ai-hive", version: SERVER_VERSION });
|
|
75
102
|
const client = () => new AiHiveApiClient({
|
|
76
103
|
endpoint: input.endpoint,
|
|
77
|
-
|
|
104
|
+
tokenProvider: input.tokenProvider,
|
|
78
105
|
timeoutMs: input.timeoutMs,
|
|
79
106
|
});
|
|
80
107
|
for (const tool of createToolDefinitions(client)) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type WorkBuddyCredentials = {
|
|
2
|
+
version: 1;
|
|
3
|
+
apiEndpoint: string;
|
|
4
|
+
webOrigin: string;
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
accessExpiresAt: string;
|
|
8
|
+
refreshExpiresAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveWorkBuddyCredentialsFile(env: Record<string, string | undefined>): string;
|
|
12
|
+
export declare class WorkBuddyCredentialStore {
|
|
13
|
+
readonly file: string;
|
|
14
|
+
private readonly now;
|
|
15
|
+
constructor(file: string, now?: () => Date);
|
|
16
|
+
read(): Promise<WorkBuddyCredentials | null>;
|
|
17
|
+
write(credentials: WorkBuddyCredentials): Promise<void>;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
withLock<T>(action: () => Promise<T>): Promise<T>;
|
|
20
|
+
private acquireLock;
|
|
21
|
+
private withGuard;
|
|
22
|
+
private acquireGuard;
|
|
23
|
+
private tryCreateOwnedLock;
|
|
24
|
+
private reclaimStaleFile;
|
|
25
|
+
private startHeartbeat;
|
|
26
|
+
private renewOwnedLock;
|
|
27
|
+
private releaseMainLock;
|
|
28
|
+
private releaseOwnedFile;
|
|
29
|
+
private pathBelongsTo;
|
|
30
|
+
private assertBeforeDeadline;
|
|
31
|
+
private waitForRetry;
|
|
32
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, open, readFile, rename, stat, unlink, } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const credentialsSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
apiEndpoint: z.string().url(),
|
|
10
|
+
webOrigin: z.string().url(),
|
|
11
|
+
accessToken: z.string().min(1),
|
|
12
|
+
refreshToken: z.string().min(1),
|
|
13
|
+
accessExpiresAt: z.string().datetime({ offset: true }),
|
|
14
|
+
refreshExpiresAt: z.string().datetime({ offset: true }),
|
|
15
|
+
updatedAt: z.string().datetime({ offset: true }),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
const LOCK_STALE_MS = 10_000;
|
|
19
|
+
const LOCK_WAIT_MS = 5_000;
|
|
20
|
+
const LOCK_POLL_MS = 50;
|
|
21
|
+
const LOCK_HEARTBEAT_MS = 2_500;
|
|
22
|
+
const LOCK_OWNER_PATTERN = /^([1-9]\d*):[a-f0-9]{32}$/;
|
|
23
|
+
function hasErrorCode(error, code) {
|
|
24
|
+
return (error instanceof Error &&
|
|
25
|
+
"code" in error &&
|
|
26
|
+
error.code === code);
|
|
27
|
+
}
|
|
28
|
+
function parseCredentials(value) {
|
|
29
|
+
const result = credentialsSchema.safeParse(value);
|
|
30
|
+
if (!result.success) {
|
|
31
|
+
throw new Error("凭证文件无效");
|
|
32
|
+
}
|
|
33
|
+
return result.data;
|
|
34
|
+
}
|
|
35
|
+
async function ensurePrivateDirectory(path) {
|
|
36
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
37
|
+
if (process.platform !== "win32") {
|
|
38
|
+
await chmod(path, 0o700);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function sleep(milliseconds) {
|
|
42
|
+
await new Promise((resolve) => {
|
|
43
|
+
setTimeout(resolve, milliseconds);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function removeIfExists(path) {
|
|
47
|
+
await unlink(path).catch((error) => {
|
|
48
|
+
if (!hasErrorCode(error, "ENOENT")) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function parseLockOwnerPid(owner) {
|
|
54
|
+
const match = LOCK_OWNER_PATTERN.exec(owner);
|
|
55
|
+
const pid = Number(match?.[1]);
|
|
56
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
57
|
+
}
|
|
58
|
+
function getProcessState(pid) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return "alive";
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (hasErrorCode(error, "EPERM")) {
|
|
65
|
+
return "alive";
|
|
66
|
+
}
|
|
67
|
+
if (hasErrorCode(error, "ESRCH")) {
|
|
68
|
+
return "dead";
|
|
69
|
+
}
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function getLockOwnerProcessState(file) {
|
|
74
|
+
let owner;
|
|
75
|
+
try {
|
|
76
|
+
owner = await readFile(file, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return hasErrorCode(error, "ENOENT") ? "missing" : "unknown";
|
|
80
|
+
}
|
|
81
|
+
const pid = parseLockOwnerPid(owner);
|
|
82
|
+
return pid === null ? "unknown" : getProcessState(pid);
|
|
83
|
+
}
|
|
84
|
+
export function resolveWorkBuddyCredentialsFile(env) {
|
|
85
|
+
return (env.AI_HIVE_WORKBUDDY_CREDENTIALS_FILE ??
|
|
86
|
+
join(homedir(), ".ai-hive", "workbuddy", "credentials.json"));
|
|
87
|
+
}
|
|
88
|
+
export class WorkBuddyCredentialStore {
|
|
89
|
+
file;
|
|
90
|
+
now;
|
|
91
|
+
constructor(file, now = () => new Date()) {
|
|
92
|
+
this.file = file;
|
|
93
|
+
this.now = now;
|
|
94
|
+
}
|
|
95
|
+
async read() {
|
|
96
|
+
let contents;
|
|
97
|
+
try {
|
|
98
|
+
contents = await readFile(this.file, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw new Error("凭证文件读取失败");
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return parseCredentials(JSON.parse(contents));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new Error("凭证文件无效");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async write(credentials) {
|
|
114
|
+
const validated = parseCredentials(credentials);
|
|
115
|
+
const parent = dirname(this.file);
|
|
116
|
+
await ensurePrivateDirectory(parent);
|
|
117
|
+
const temporaryFile = join(parent, `.${basename(this.file)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
118
|
+
let handle;
|
|
119
|
+
try {
|
|
120
|
+
handle = await open(temporaryFile, "wx", 0o600);
|
|
121
|
+
if (process.platform !== "win32") {
|
|
122
|
+
await chmod(temporaryFile, 0o600);
|
|
123
|
+
}
|
|
124
|
+
await handle.writeFile(JSON.stringify(validated), "utf8");
|
|
125
|
+
await handle.sync();
|
|
126
|
+
await handle.close();
|
|
127
|
+
handle = undefined;
|
|
128
|
+
await rename(temporaryFile, this.file);
|
|
129
|
+
if (process.platform !== "win32") {
|
|
130
|
+
await chmod(this.file, 0o600).catch(() => undefined);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
await handle?.close().catch(() => undefined);
|
|
135
|
+
await removeIfExists(temporaryFile).catch(() => undefined);
|
|
136
|
+
throw new Error("凭证文件写入失败");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async clear() {
|
|
140
|
+
try {
|
|
141
|
+
await removeIfExists(this.file);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error("凭证文件删除失败");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async withLock(action) {
|
|
148
|
+
const lockFile = `${this.file}.lock`;
|
|
149
|
+
const deadline = this.now().getTime() + LOCK_WAIT_MS;
|
|
150
|
+
await ensurePrivateDirectory(dirname(lockFile));
|
|
151
|
+
const ownedLock = await this.acquireLock(lockFile, deadline);
|
|
152
|
+
const stopHeartbeat = this.startHeartbeat(lockFile, ownedLock);
|
|
153
|
+
try {
|
|
154
|
+
return await action();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await stopHeartbeat();
|
|
158
|
+
await this.releaseMainLock(lockFile, ownedLock);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async acquireLock(lockFile, deadline) {
|
|
162
|
+
while (true) {
|
|
163
|
+
this.assertBeforeDeadline(deadline);
|
|
164
|
+
let acquired = null;
|
|
165
|
+
await this.withGuard(lockFile, deadline, async () => {
|
|
166
|
+
this.assertBeforeDeadline(deadline);
|
|
167
|
+
acquired = await this.tryCreateOwnedLock(lockFile);
|
|
168
|
+
if (!acquired) {
|
|
169
|
+
await this.reclaimStaleFile(lockFile);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (acquired) {
|
|
173
|
+
return acquired;
|
|
174
|
+
}
|
|
175
|
+
await this.waitForRetry(deadline);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async withGuard(lockFile, deadline, action) {
|
|
179
|
+
const guardFile = `${lockFile}.guard`;
|
|
180
|
+
const guard = await this.acquireGuard(guardFile, deadline);
|
|
181
|
+
try {
|
|
182
|
+
return await action();
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await this.releaseOwnedFile(guardFile, guard);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async acquireGuard(guardFile, deadline) {
|
|
189
|
+
while (true) {
|
|
190
|
+
this.assertBeforeDeadline(deadline);
|
|
191
|
+
const guard = await this.tryCreateOwnedLock(guardFile);
|
|
192
|
+
if (guard) {
|
|
193
|
+
return guard;
|
|
194
|
+
}
|
|
195
|
+
await this.waitForRetry(deadline);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async tryCreateOwnedLock(file) {
|
|
199
|
+
const owner = `${process.pid}:${randomBytes(16).toString("hex")}`;
|
|
200
|
+
let handle;
|
|
201
|
+
try {
|
|
202
|
+
handle = await open(file, "wx", 0o600);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
throw new Error("凭证文件锁操作失败");
|
|
209
|
+
}
|
|
210
|
+
const owned = { handle, owner };
|
|
211
|
+
try {
|
|
212
|
+
await handle.writeFile(owner, "utf8");
|
|
213
|
+
await handle.sync();
|
|
214
|
+
return owned;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
await this.releaseOwnedFile(file, owned);
|
|
218
|
+
throw new Error("凭证文件锁操作失败");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async reclaimStaleFile(file) {
|
|
222
|
+
let fileStats;
|
|
223
|
+
try {
|
|
224
|
+
fileStats = await stat(file);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
throw new Error("凭证文件锁操作失败");
|
|
231
|
+
}
|
|
232
|
+
if (this.now().getTime() - fileStats.mtimeMs < LOCK_STALE_MS) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const ownerState = await getLockOwnerProcessState(file);
|
|
236
|
+
if (ownerState === "missing") {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (ownerState !== "dead") {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await unlink(file);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
throw new Error("凭证文件锁操作失败");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
startHeartbeat(lockFile, ownedLock) {
|
|
254
|
+
let stopped = false;
|
|
255
|
+
let timer;
|
|
256
|
+
let pending = Promise.resolve();
|
|
257
|
+
const schedule = () => {
|
|
258
|
+
timer = setTimeout(() => {
|
|
259
|
+
pending = this.renewOwnedLock(lockFile, ownedLock).catch(() => undefined);
|
|
260
|
+
return pending.finally(() => {
|
|
261
|
+
if (!stopped) {
|
|
262
|
+
schedule();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}, LOCK_HEARTBEAT_MS);
|
|
266
|
+
};
|
|
267
|
+
schedule();
|
|
268
|
+
return async () => {
|
|
269
|
+
stopped = true;
|
|
270
|
+
if (timer) {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
}
|
|
273
|
+
await pending;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async renewOwnedLock(lockFile, ownedLock) {
|
|
277
|
+
const deadline = this.now().getTime() + LOCK_WAIT_MS;
|
|
278
|
+
await this.withGuard(lockFile, deadline, async () => {
|
|
279
|
+
if (!(await this.pathBelongsTo(lockFile, ownedLock))) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const refreshedAt = this.now();
|
|
283
|
+
await ownedLock.handle.utimes(refreshedAt, refreshedAt);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async releaseMainLock(lockFile, ownedLock) {
|
|
287
|
+
const deadline = this.now().getTime() + LOCK_WAIT_MS;
|
|
288
|
+
try {
|
|
289
|
+
await this.withGuard(lockFile, deadline, async () => {
|
|
290
|
+
await this.releaseOwnedFile(lockFile, ownedLock);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
await ownedLock.handle.close().catch(() => undefined);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async releaseOwnedFile(file, ownedLock) {
|
|
298
|
+
const heldStats = await ownedLock.handle.stat().catch(() => undefined);
|
|
299
|
+
await ownedLock.handle.close().catch(() => undefined);
|
|
300
|
+
if (!heldStats) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
if (!(await this.pathBelongsTo(file, ownedLock, heldStats))) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await unlink(file);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async pathBelongsTo(file, ownedLock, heldStats) {
|
|
314
|
+
const expectedStats = heldStats ?? (await ownedLock.handle.stat().catch(() => undefined));
|
|
315
|
+
if (!expectedStats) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const pathStats = await stat(file);
|
|
320
|
+
return (pathStats.dev === expectedStats.dev &&
|
|
321
|
+
pathStats.ino === expectedStats.ino &&
|
|
322
|
+
(await readFile(file, "utf8")) === ownedLock.owner);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
assertBeforeDeadline(deadline) {
|
|
329
|
+
if (this.now().getTime() >= deadline) {
|
|
330
|
+
throw new Error("凭证文件锁等待超时");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async waitForRetry(deadline) {
|
|
334
|
+
const remaining = deadline - this.now().getTime();
|
|
335
|
+
if (remaining <= 0) {
|
|
336
|
+
throw new Error("凭证文件锁等待超时");
|
|
337
|
+
}
|
|
338
|
+
await sleep(Math.min(LOCK_POLL_MS, remaining));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LoopbackListener = {
|
|
2
|
+
redirectUri: string;
|
|
3
|
+
completion: Promise<void>;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare function listenForWorkBuddyCallback(_input: {
|
|
7
|
+
expectedState: string;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
onCode(code: string): Promise<void>;
|
|
10
|
+
}): Promise<LoopbackListener>;
|