@runapi.ai/mcp-core 0.1.3 → 0.1.5
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 +201 -201
- package/README.md +118 -0
- package/dist/config.d.ts +14 -1
- package/dist/config.js +16 -9
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/login.d.ts +68 -0
- package/dist/login.js +579 -0
- package/dist/login.js.map +1 -0
- package/dist/runapi-client.d.ts +5 -2
- package/dist/runapi-client.js +9 -5
- package/dist/runapi-client.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAG/B;IACA;IAHX,YACE,OAAe,EACN,MAAe,EACf,OAAiB;QAE1B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAS;QACf,YAAO,GAAP,OAAO,CAAU;QAG1B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;QACvC,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;YACrB,KAAK,GAAG;gBACN,OAAO,
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./tool-response.js";
|
|
|
7
7
|
export * from "./schema.js";
|
|
8
8
|
export * from "./contract.js";
|
|
9
9
|
export * from "./pricing.js";
|
|
10
|
+
export * from "./login.js";
|
|
10
11
|
export { validateInputRules } from "./input-rules.js";
|
|
11
12
|
export * from "./runapi-client.js";
|
|
12
13
|
export * from "./server.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./tool-response.js";
|
|
|
7
7
|
export * from "./schema.js";
|
|
8
8
|
export * from "./contract.js";
|
|
9
9
|
export * from "./pricing.js";
|
|
10
|
+
export * from "./login.js";
|
|
10
11
|
export { validateInputRules } from "./input-rules.js";
|
|
11
12
|
export * from "./runapi-client.js";
|
|
12
13
|
export * from "./server.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC"}
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export type LoginInput = {
|
|
3
|
+
force?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type LoginResult = {
|
|
6
|
+
authenticated: boolean;
|
|
7
|
+
source?: "env" | "config";
|
|
8
|
+
base_url: string;
|
|
9
|
+
config_path: string;
|
|
10
|
+
wrote_config: boolean;
|
|
11
|
+
verified?: boolean;
|
|
12
|
+
browser_opened?: boolean;
|
|
13
|
+
open_browser_error?: string;
|
|
14
|
+
authorize_url?: string;
|
|
15
|
+
callback_url?: string;
|
|
16
|
+
callback_port?: number;
|
|
17
|
+
display_code?: string;
|
|
18
|
+
expires_in_seconds?: number;
|
|
19
|
+
user?: {
|
|
20
|
+
email?: string;
|
|
21
|
+
};
|
|
22
|
+
token_name?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
};
|
|
26
|
+
export type CallbackPayload = {
|
|
27
|
+
code?: string;
|
|
28
|
+
state?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
error_description?: string;
|
|
31
|
+
};
|
|
32
|
+
export type CallbackListener = {
|
|
33
|
+
port: number;
|
|
34
|
+
wait: () => Promise<CallbackPayload>;
|
|
35
|
+
close: () => Promise<void> | void;
|
|
36
|
+
};
|
|
37
|
+
export type LoginPendingPayload = Pick<LoginResult, "authorize_url" | "callback_url" | "callback_port" | "display_code" | "expires_in_seconds" | "browser_opened" | "open_browser_error">;
|
|
38
|
+
export type LoginDependencies = {
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
configFile?: string;
|
|
41
|
+
fetchImpl?: typeof fetch;
|
|
42
|
+
hostname?: string;
|
|
43
|
+
userAgent?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
openBrowserTimeoutMs?: number;
|
|
46
|
+
randomBytes?: (size: number) => Buffer;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
49
|
+
createCallbackListener?: (options: {
|
|
50
|
+
state: string;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
}) => Promise<CallbackListener>;
|
|
53
|
+
onPending?: (payload: LoginPendingPayload) => Promise<void> | void;
|
|
54
|
+
};
|
|
55
|
+
export type PkcePair = {
|
|
56
|
+
codeVerifier: string;
|
|
57
|
+
codeChallenge: string;
|
|
58
|
+
codeChallengeMethod: "S256";
|
|
59
|
+
};
|
|
60
|
+
export declare function generatePkcePair(bytes?: Uint8Array): PkcePair;
|
|
61
|
+
export declare function generateDisplayCode(bytes?: Uint8Array): string;
|
|
62
|
+
export declare function login(input?: LoginInput, deps?: LoginDependencies): Promise<LoginResult>;
|
|
63
|
+
export declare function registerLoginTool(server: McpServer, deps?: LoginDependencies): void;
|
|
64
|
+
export declare function openSystemBrowser(url: string, platform?: NodeJS.Platform, timeoutMs?: number): Promise<void>;
|
|
65
|
+
export declare function browserCommand(url: string, platform: NodeJS.Platform): {
|
|
66
|
+
command: string;
|
|
67
|
+
args: string[];
|
|
68
|
+
} | undefined;
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { DEFAULT_BASE_URL, RUNAPI_API_KEY_ENV, USER_AGENT } from "./constants.js";
|
|
9
|
+
import { configPath, loadConfigDetails, readConfigFile } from "./config.js";
|
|
10
|
+
import { errorFromResponse, friendlyError, RunApiClientError } from "./errors.js";
|
|
11
|
+
import { RunApiClient } from "./runapi-client.js";
|
|
12
|
+
import { jsonText } from "./tool-response.js";
|
|
13
|
+
const LOGIN_TIMEOUT_MS = 300_000;
|
|
14
|
+
const OPEN_BROWSER_TIMEOUT_MS = 10_000;
|
|
15
|
+
const DISPLAY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
16
|
+
class LoginCancelledError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("RunAPI login was cancelled.");
|
|
19
|
+
this.name = "LoginCancelledError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function generatePkcePair(bytes = crypto.randomBytes(32)) {
|
|
23
|
+
const codeVerifier = base64Url(bytes);
|
|
24
|
+
return {
|
|
25
|
+
codeVerifier,
|
|
26
|
+
codeChallenge: base64Url(crypto.createHash("sha256").update(codeVerifier).digest()),
|
|
27
|
+
codeChallengeMethod: "S256"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function generateDisplayCode(bytes = crypto.randomBytes(5)) {
|
|
31
|
+
let buffer = 0;
|
|
32
|
+
let bits = 0;
|
|
33
|
+
let output = "";
|
|
34
|
+
for (const byte of bytes) {
|
|
35
|
+
buffer = (buffer << 8) | byte;
|
|
36
|
+
bits += 8;
|
|
37
|
+
while (bits >= 5 && output.length < 8) {
|
|
38
|
+
bits -= 5;
|
|
39
|
+
output += DISPLAY_CODE_ALPHABET[(buffer >> bits) & 31];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
while (output.length < 8) {
|
|
43
|
+
output += DISPLAY_CODE_ALPHABET[0];
|
|
44
|
+
}
|
|
45
|
+
return `${output.slice(0, 4)}-${output.slice(4, 8)}`;
|
|
46
|
+
}
|
|
47
|
+
export async function login(input = {}, deps = {}) {
|
|
48
|
+
const env = deps.env ?? process.env;
|
|
49
|
+
const configFile = deps.configFile ?? configPath();
|
|
50
|
+
const config = loadConfigDetails(env, configFile);
|
|
51
|
+
const baseResult = {
|
|
52
|
+
base_url: config.baseUrl,
|
|
53
|
+
config_path: configFile,
|
|
54
|
+
wrote_config: false
|
|
55
|
+
};
|
|
56
|
+
if (config.apiKeySource === "env") {
|
|
57
|
+
if (input.force) {
|
|
58
|
+
return {
|
|
59
|
+
...baseResult,
|
|
60
|
+
authenticated: true,
|
|
61
|
+
source: "env",
|
|
62
|
+
hint: `${RUNAPI_API_KEY_ENV} is set and takes precedence over local config. Unset or update it before forcing browser login.`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...baseResult,
|
|
67
|
+
authenticated: true,
|
|
68
|
+
source: "env",
|
|
69
|
+
hint: `Using ${RUNAPI_API_KEY_ENV}. If authenticated tools fail, update or unset the environment variable.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (config.apiKeySource === "config" && !input.force) {
|
|
73
|
+
return {
|
|
74
|
+
...baseResult,
|
|
75
|
+
authenticated: true,
|
|
76
|
+
source: "config"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const randomBytes = deps.randomBytes ?? ((size) => crypto.randomBytes(size));
|
|
80
|
+
const state = base64Url(randomBytes(32));
|
|
81
|
+
const pkce = generatePkcePair(randomBytes(32));
|
|
82
|
+
const displayCode = generateDisplayCode(randomBytes(5));
|
|
83
|
+
const timeoutMs = deps.timeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
84
|
+
const hostname = deps.hostname ?? (os.hostname() || "device");
|
|
85
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
86
|
+
const userAgent = deps.userAgent ?? USER_AGENT;
|
|
87
|
+
const signal = deps.signal;
|
|
88
|
+
const openBrowserTimeoutMs = deps.openBrowserTimeoutMs ?? OPEN_BROWSER_TIMEOUT_MS;
|
|
89
|
+
let listener;
|
|
90
|
+
let callbackUrl;
|
|
91
|
+
let authorizeUrl;
|
|
92
|
+
let browserOpened;
|
|
93
|
+
let openBrowserError;
|
|
94
|
+
try {
|
|
95
|
+
throwIfAborted(signal);
|
|
96
|
+
listener = await (deps.createCallbackListener ?? createLoopbackCallbackListener)({ state, timeoutMs });
|
|
97
|
+
callbackUrl = `http://127.0.0.1:${listener.port}/callback`;
|
|
98
|
+
authorizeUrl = buildAuthorizeUrl(config.baseUrl, {
|
|
99
|
+
state,
|
|
100
|
+
codeChallenge: pkce.codeChallenge,
|
|
101
|
+
redirectPort: listener.port,
|
|
102
|
+
displayCode,
|
|
103
|
+
hostname
|
|
104
|
+
});
|
|
105
|
+
browserOpened = true;
|
|
106
|
+
try {
|
|
107
|
+
await withCancellation(withTimeout((deps.openBrowser ?? openSystemBrowser)(authorizeUrl), openBrowserTimeoutMs, "Opening the browser timed out."), signal);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof LoginCancelledError) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
browserOpened = false;
|
|
114
|
+
openBrowserError = friendlyError(error);
|
|
115
|
+
await notifyPending(deps.onPending, pendingPayload({
|
|
116
|
+
authorizeUrl,
|
|
117
|
+
callbackUrl,
|
|
118
|
+
callbackPort: listener.port,
|
|
119
|
+
displayCode,
|
|
120
|
+
timeoutMs,
|
|
121
|
+
browserOpened,
|
|
122
|
+
openBrowserError
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
const callback = await withCancellation(listener.wait(), signal);
|
|
126
|
+
if (callback.state !== state) {
|
|
127
|
+
return {
|
|
128
|
+
...baseResult,
|
|
129
|
+
authenticated: false,
|
|
130
|
+
browser_opened: browserOpened,
|
|
131
|
+
open_browser_error: openBrowserError,
|
|
132
|
+
authorize_url: authorizeUrl,
|
|
133
|
+
callback_url: callbackUrl,
|
|
134
|
+
callback_port: listener.port,
|
|
135
|
+
display_code: displayCode,
|
|
136
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
137
|
+
error: "Authorization callback state did not match.",
|
|
138
|
+
hint: "Retry login and only approve the browser window opened for this request."
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (callback.error) {
|
|
142
|
+
return {
|
|
143
|
+
...baseResult,
|
|
144
|
+
authenticated: false,
|
|
145
|
+
browser_opened: browserOpened,
|
|
146
|
+
open_browser_error: openBrowserError,
|
|
147
|
+
authorize_url: authorizeUrl,
|
|
148
|
+
callback_url: callbackUrl,
|
|
149
|
+
callback_port: listener.port,
|
|
150
|
+
display_code: displayCode,
|
|
151
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
152
|
+
error: callback.error_description || callback.error
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (!callback.code) {
|
|
156
|
+
return {
|
|
157
|
+
...baseResult,
|
|
158
|
+
authenticated: false,
|
|
159
|
+
browser_opened: browserOpened,
|
|
160
|
+
open_browser_error: openBrowserError,
|
|
161
|
+
authorize_url: authorizeUrl,
|
|
162
|
+
callback_url: callbackUrl,
|
|
163
|
+
callback_port: listener.port,
|
|
164
|
+
display_code: displayCode,
|
|
165
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
166
|
+
error: "Authorization callback did not include a code."
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
throwIfAborted(signal);
|
|
170
|
+
const exchanged = await exchangeCode({
|
|
171
|
+
baseUrl: config.baseUrl,
|
|
172
|
+
code: callback.code,
|
|
173
|
+
codeVerifier: pkce.codeVerifier,
|
|
174
|
+
redirectPort: listener.port,
|
|
175
|
+
hostname,
|
|
176
|
+
userAgent,
|
|
177
|
+
fetchImpl
|
|
178
|
+
});
|
|
179
|
+
if (!exchanged.key) {
|
|
180
|
+
return {
|
|
181
|
+
...baseResult,
|
|
182
|
+
authenticated: false,
|
|
183
|
+
browser_opened: browserOpened,
|
|
184
|
+
open_browser_error: openBrowserError,
|
|
185
|
+
authorize_url: authorizeUrl,
|
|
186
|
+
callback_url: callbackUrl,
|
|
187
|
+
callback_port: listener.port,
|
|
188
|
+
display_code: displayCode,
|
|
189
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
190
|
+
error: "RunAPI did not return an API key."
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
throwIfAborted(signal);
|
|
194
|
+
await writeConfigApiKey(configFile, exchanged.key);
|
|
195
|
+
try {
|
|
196
|
+
await new RunApiClient({ apiKey: exchanged.key, baseUrl: config.baseUrl }, fetchImpl, userAgent).balance();
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
...baseResult,
|
|
201
|
+
authenticated: false,
|
|
202
|
+
source: "config",
|
|
203
|
+
wrote_config: true,
|
|
204
|
+
verified: false,
|
|
205
|
+
browser_opened: browserOpened,
|
|
206
|
+
open_browser_error: openBrowserError,
|
|
207
|
+
authorize_url: authorizeUrl,
|
|
208
|
+
callback_url: callbackUrl,
|
|
209
|
+
callback_port: listener.port,
|
|
210
|
+
display_code: displayCode,
|
|
211
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
212
|
+
user: exchanged.user,
|
|
213
|
+
token_name: exchanged.name,
|
|
214
|
+
error: friendlyError(error),
|
|
215
|
+
hint: "The token was saved, but verification failed. Retry an authenticated tool or run login again."
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
...baseResult,
|
|
220
|
+
authenticated: true,
|
|
221
|
+
source: "config",
|
|
222
|
+
wrote_config: true,
|
|
223
|
+
verified: true,
|
|
224
|
+
browser_opened: browserOpened,
|
|
225
|
+
open_browser_error: openBrowserError,
|
|
226
|
+
authorize_url: authorizeUrl,
|
|
227
|
+
callback_url: callbackUrl,
|
|
228
|
+
callback_port: listener.port,
|
|
229
|
+
display_code: displayCode,
|
|
230
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
231
|
+
user: exchanged.user,
|
|
232
|
+
token_name: exchanged.name
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return {
|
|
237
|
+
...baseResult,
|
|
238
|
+
authenticated: false,
|
|
239
|
+
browser_opened: browserOpened,
|
|
240
|
+
open_browser_error: openBrowserError,
|
|
241
|
+
authorize_url: authorizeUrl,
|
|
242
|
+
callback_url: callbackUrl,
|
|
243
|
+
callback_port: listener?.port,
|
|
244
|
+
display_code: displayCode,
|
|
245
|
+
expires_in_seconds: Math.floor(timeoutMs / 1000),
|
|
246
|
+
error: friendlyLoginError(error)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await listener?.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export function registerLoginTool(server, deps = {}) {
|
|
254
|
+
server.tool("login", "Authenticate RunAPI by opening a browser PKCE login flow and saving the API key to ~/.config/runapi/config.json.", {
|
|
255
|
+
force: z.boolean().default(false).describe("Re-run browser login when the current credential comes from the local config file.")
|
|
256
|
+
}, async ({ force }, extra) => jsonText(await login({ force }, {
|
|
257
|
+
...deps,
|
|
258
|
+
signal: extra.signal,
|
|
259
|
+
onPending: async (payload) => {
|
|
260
|
+
await notifyPending(deps.onPending, payload);
|
|
261
|
+
await Promise.allSettled([
|
|
262
|
+
extra.sendNotification({
|
|
263
|
+
method: "notifications/message",
|
|
264
|
+
params: {
|
|
265
|
+
level: "notice",
|
|
266
|
+
logger: "runapi.login",
|
|
267
|
+
data: {
|
|
268
|
+
message: "Open this RunAPI login URL in your browser to continue authentication.",
|
|
269
|
+
...payload
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}),
|
|
273
|
+
extra.sendNotification({
|
|
274
|
+
method: "notifications/progress",
|
|
275
|
+
params: {
|
|
276
|
+
progressToken: extra._meta?.progressToken ?? "runapi-login",
|
|
277
|
+
progress: 0,
|
|
278
|
+
total: payload.expires_in_seconds ?? Math.floor(LOGIN_TIMEOUT_MS / 1000),
|
|
279
|
+
message: `RunAPI login URL: ${payload.authorize_url}`
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
})));
|
|
285
|
+
}
|
|
286
|
+
export async function openSystemBrowser(url, platform = process.platform, timeoutMs = OPEN_BROWSER_TIMEOUT_MS) {
|
|
287
|
+
const command = browserCommand(url, platform);
|
|
288
|
+
if (!command) {
|
|
289
|
+
throw new Error(`Opening a browser is not supported on ${platform}.`);
|
|
290
|
+
}
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
let settled = false;
|
|
293
|
+
const child = spawn(command.command, command.args, { stdio: "ignore", shell: false });
|
|
294
|
+
const timeout = setTimeout(() => {
|
|
295
|
+
finish(new Error("Opening the browser timed out."));
|
|
296
|
+
child.kill();
|
|
297
|
+
}, timeoutMs);
|
|
298
|
+
function finish(error) {
|
|
299
|
+
if (settled) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
settled = true;
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
if (error) {
|
|
305
|
+
reject(error);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
resolve();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
child.once("error", finish);
|
|
312
|
+
child.once("close", (code) => {
|
|
313
|
+
if (code === 0) {
|
|
314
|
+
finish();
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
finish(new Error(`${command.command} exited with status ${code ?? "unknown"}.`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
export function browserCommand(url, platform) {
|
|
323
|
+
switch (platform) {
|
|
324
|
+
case "darwin":
|
|
325
|
+
return { command: "open", args: [url] };
|
|
326
|
+
case "linux":
|
|
327
|
+
return { command: "xdg-open", args: [url] };
|
|
328
|
+
case "win32":
|
|
329
|
+
return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
|
|
330
|
+
default:
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function exchangeCode(options) {
|
|
335
|
+
const response = await options.fetchImpl(new URL("/api/v1/cli/exchange", trimBaseUrl(options.baseUrl)), {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
accept: "application/json",
|
|
339
|
+
"content-type": "application/json",
|
|
340
|
+
"user-agent": options.userAgent,
|
|
341
|
+
"x-cli-hostname": options.hostname
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
code: options.code,
|
|
345
|
+
code_verifier: options.codeVerifier,
|
|
346
|
+
redirect_port: options.redirectPort
|
|
347
|
+
})
|
|
348
|
+
});
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
throw await errorFromResponse(response);
|
|
351
|
+
}
|
|
352
|
+
return await response.json();
|
|
353
|
+
}
|
|
354
|
+
function buildAuthorizeUrl(baseUrl, params) {
|
|
355
|
+
const url = new URL("/cli/authorize", trimBaseUrl(baseUrl));
|
|
356
|
+
url.searchParams.set("state", params.state);
|
|
357
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
358
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
359
|
+
url.searchParams.set("redirect_port", String(params.redirectPort));
|
|
360
|
+
url.searchParams.set("display_code", params.displayCode);
|
|
361
|
+
url.searchParams.set("hostname", params.hostname);
|
|
362
|
+
return url.toString();
|
|
363
|
+
}
|
|
364
|
+
async function writeConfigApiKey(filePath, apiKey) {
|
|
365
|
+
const dir = path.dirname(filePath);
|
|
366
|
+
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
367
|
+
await chmodIfSupported(dir, 0o700);
|
|
368
|
+
const existing = readConfigObject(filePath);
|
|
369
|
+
const next = {
|
|
370
|
+
...existing,
|
|
371
|
+
api_key: apiKey
|
|
372
|
+
};
|
|
373
|
+
delete next.apiKey;
|
|
374
|
+
const tmp = path.join(dir, `.config.json.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
375
|
+
try {
|
|
376
|
+
await fs.promises.writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
377
|
+
await chmodIfSupported(tmp, 0o600);
|
|
378
|
+
await fs.promises.rename(tmp, filePath);
|
|
379
|
+
await chmodIfSupported(filePath, 0o600);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
await removeTempFile(tmp);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function readConfigObject(filePath) {
|
|
387
|
+
const parsed = readConfigFile(filePath);
|
|
388
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
389
|
+
}
|
|
390
|
+
function pendingPayload(options) {
|
|
391
|
+
return {
|
|
392
|
+
authorize_url: options.authorizeUrl,
|
|
393
|
+
callback_url: options.callbackUrl,
|
|
394
|
+
callback_port: options.callbackPort,
|
|
395
|
+
display_code: options.displayCode,
|
|
396
|
+
expires_in_seconds: Math.floor(options.timeoutMs / 1000),
|
|
397
|
+
browser_opened: options.browserOpened,
|
|
398
|
+
open_browser_error: options.openBrowserError
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function notifyPending(onPending, payload) {
|
|
402
|
+
try {
|
|
403
|
+
await onPending?.(payload);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Best effort only; authentication should continue even if the host cannot
|
|
407
|
+
// surface the manual login URL notification.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function friendlyLoginError(error) {
|
|
411
|
+
if (error instanceof RunApiClientError) {
|
|
412
|
+
return friendlyError(error);
|
|
413
|
+
}
|
|
414
|
+
if (error instanceof LoginCancelledError) {
|
|
415
|
+
return error.message;
|
|
416
|
+
}
|
|
417
|
+
if (error instanceof Error && error.message === "Timed out waiting for browser authorization.") {
|
|
418
|
+
return error.message;
|
|
419
|
+
}
|
|
420
|
+
return "RunAPI login could not complete.";
|
|
421
|
+
}
|
|
422
|
+
function throwIfAborted(signal) {
|
|
423
|
+
if (signal?.aborted) {
|
|
424
|
+
throw new LoginCancelledError();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function withCancellation(promise, signal) {
|
|
428
|
+
if (!signal) {
|
|
429
|
+
return promise;
|
|
430
|
+
}
|
|
431
|
+
throwIfAborted(signal);
|
|
432
|
+
return await new Promise((resolve, reject) => {
|
|
433
|
+
const abort = () => reject(new LoginCancelledError());
|
|
434
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
435
|
+
promise.then(resolve, reject).finally(() => {
|
|
436
|
+
signal.removeEventListener("abort", abort);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
441
|
+
let timeout;
|
|
442
|
+
try {
|
|
443
|
+
return await Promise.race([
|
|
444
|
+
promise,
|
|
445
|
+
new Promise((_, reject) => {
|
|
446
|
+
timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
447
|
+
})
|
|
448
|
+
]);
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
clearTimeout(timeout);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function removeTempFile(filePath) {
|
|
455
|
+
try {
|
|
456
|
+
await fs.promises.rm(filePath, { force: true });
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Best effort cleanup; preserve the original config write error.
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function chmodIfSupported(filePath, mode) {
|
|
463
|
+
if (process.platform === "win32") {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
await fs.promises.chmod(filePath, mode);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Best effort only; the login flow should not fail on filesystems that
|
|
471
|
+
// cannot apply POSIX modes.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function createLoopbackCallbackListener(options) {
|
|
475
|
+
let settled = false;
|
|
476
|
+
let resolvePayload;
|
|
477
|
+
let rejectPayload;
|
|
478
|
+
let timeout;
|
|
479
|
+
const waitPromise = new Promise((resolve, reject) => {
|
|
480
|
+
resolvePayload = resolve;
|
|
481
|
+
rejectPayload = reject;
|
|
482
|
+
});
|
|
483
|
+
const server = http.createServer((request, response) => {
|
|
484
|
+
const payload = parseCallback(request, serverAddressPort(server));
|
|
485
|
+
if (!payload) {
|
|
486
|
+
writeCallbackHtml(response, 404, "Not found.");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (payload.state !== options.state) {
|
|
490
|
+
writeCallbackHtml(response, 400, "Authorization failed. You can close this tab.");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (payload.error) {
|
|
494
|
+
writeCallbackHtml(response, 400, "Authorization canceled. You can close this tab.");
|
|
495
|
+
settle(payload);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
writeCallbackHtml(response, 200, "Authorization received. You can close this tab and return to your MCP client.");
|
|
499
|
+
settle(payload);
|
|
500
|
+
});
|
|
501
|
+
const port = await new Promise((resolve, reject) => {
|
|
502
|
+
server.once("error", reject);
|
|
503
|
+
server.listen(0, "127.0.0.1", () => resolve(serverAddressPort(server)));
|
|
504
|
+
});
|
|
505
|
+
timeout = setTimeout(() => {
|
|
506
|
+
settleError(new Error("Timed out waiting for browser authorization."));
|
|
507
|
+
}, options.timeoutMs);
|
|
508
|
+
function settle(payload) {
|
|
509
|
+
if (settled) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
settled = true;
|
|
513
|
+
clearTimeout(timeout);
|
|
514
|
+
resolvePayload(payload);
|
|
515
|
+
}
|
|
516
|
+
function settleError(error) {
|
|
517
|
+
if (settled) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
settled = true;
|
|
521
|
+
clearTimeout(timeout);
|
|
522
|
+
rejectPayload(error);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
port,
|
|
526
|
+
wait: () => waitPromise,
|
|
527
|
+
close: async () => {
|
|
528
|
+
clearTimeout(timeout);
|
|
529
|
+
await new Promise((resolve) => {
|
|
530
|
+
server.close(() => resolve());
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function parseCallback(request, port) {
|
|
536
|
+
if (!request.url) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
const url = new URL(request.url, `http://127.0.0.1:${port}`);
|
|
540
|
+
if (url.pathname !== "/callback") {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
code: url.searchParams.get("code") || undefined,
|
|
545
|
+
state: url.searchParams.get("state") || undefined,
|
|
546
|
+
error: url.searchParams.get("error") || undefined,
|
|
547
|
+
error_description: url.searchParams.get("error_description") || undefined
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function writeCallbackHtml(response, status, message) {
|
|
551
|
+
response.writeHead(status, {
|
|
552
|
+
"content-type": "text/html; charset=utf-8",
|
|
553
|
+
"cache-control": "no-store",
|
|
554
|
+
"access-control-allow-origin": "*"
|
|
555
|
+
});
|
|
556
|
+
response.end(`<!doctype html><html><head><meta charset="utf-8"><title>RunAPI authorization</title></head><body><p>${escapeHtml(message)}</p></body></html>`);
|
|
557
|
+
}
|
|
558
|
+
function serverAddressPort(server) {
|
|
559
|
+
const address = server.address();
|
|
560
|
+
if (!address || typeof address === "string") {
|
|
561
|
+
throw new Error("Unable to determine callback listener port.");
|
|
562
|
+
}
|
|
563
|
+
return address.port;
|
|
564
|
+
}
|
|
565
|
+
function base64Url(bytes) {
|
|
566
|
+
return Buffer.from(bytes).toString("base64url");
|
|
567
|
+
}
|
|
568
|
+
function trimBaseUrl(baseUrl) {
|
|
569
|
+
return baseUrl.replace(/\/+$/, "") || DEFAULT_BASE_URL;
|
|
570
|
+
}
|
|
571
|
+
function escapeHtml(value) {
|
|
572
|
+
return value
|
|
573
|
+
.replace(/&/g, "&")
|
|
574
|
+
.replace(/</g, "<")
|
|
575
|
+
.replace(/>/g, ">")
|
|
576
|
+
.replace(/"/g, """)
|
|
577
|
+
.replace(/'/g, "'");
|
|
578
|
+
}
|
|
579
|
+
//# sourceMappingURL=login.js.map
|