integrate-sdk 0.6.5 → 0.6.6
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/adapters/auto-routes.js +268 -0
- package/dist/adapters/base-handler.js +144 -0
- package/dist/adapters/nextjs-oauth-redirect.js +152 -0
- package/dist/adapters/nextjs.js +405 -0
- package/dist/adapters/node.js +280 -0
- package/dist/adapters/solid-start.js +27 -0
- package/dist/adapters/svelte-kit.js +34 -0
- package/dist/adapters/tanstack-start.js +26 -0
- package/dist/index.js +169 -70
- package/dist/src/adapters/node.d.ts +48 -0
- package/dist/src/adapters/node.d.ts.map +1 -0
- package/dist/src/adapters/solid-start.d.ts +54 -0
- package/dist/src/adapters/solid-start.d.ts.map +1 -0
- package/dist/src/adapters/svelte-kit.d.ts +82 -0
- package/dist/src/adapters/svelte-kit.d.ts.map +1 -0
- package/dist/src/adapters/tanstack-start.d.ts +28 -144
- package/dist/src/adapters/tanstack-start.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +28 -7
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/oauth/pkce.ts
|
|
13
|
+
var exports_pkce = {};
|
|
14
|
+
__export(exports_pkce, {
|
|
15
|
+
parseState: () => parseState,
|
|
16
|
+
generateStateWithReturnUrl: () => generateStateWithReturnUrl,
|
|
17
|
+
generateState: () => generateState,
|
|
18
|
+
generateCodeVerifier: () => generateCodeVerifier,
|
|
19
|
+
generateCodeChallenge: () => generateCodeChallenge
|
|
20
|
+
});
|
|
21
|
+
function generateCodeVerifier() {
|
|
22
|
+
const array = new Uint8Array(32);
|
|
23
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
24
|
+
crypto.getRandomValues(array);
|
|
25
|
+
} else if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
26
|
+
globalThis.crypto.getRandomValues(array);
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error("crypto.getRandomValues is not available. Please use Node.js 19+ or a modern browser.");
|
|
29
|
+
}
|
|
30
|
+
return base64UrlEncode(array);
|
|
31
|
+
}
|
|
32
|
+
async function generateCodeChallenge(verifier) {
|
|
33
|
+
const encoder = new TextEncoder;
|
|
34
|
+
const data = encoder.encode(verifier);
|
|
35
|
+
let hashBuffer;
|
|
36
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
37
|
+
hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
38
|
+
} else if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
|
|
39
|
+
hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error("crypto.subtle.digest is not available. Please use Node.js 19+ or a modern browser.");
|
|
42
|
+
}
|
|
43
|
+
return base64UrlEncode(new Uint8Array(hashBuffer));
|
|
44
|
+
}
|
|
45
|
+
function generateState() {
|
|
46
|
+
const array = new Uint8Array(16);
|
|
47
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
48
|
+
crypto.getRandomValues(array);
|
|
49
|
+
} else if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
50
|
+
globalThis.crypto.getRandomValues(array);
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error("crypto.getRandomValues is not available. Please use Node.js 19+ or a modern browser.");
|
|
53
|
+
}
|
|
54
|
+
return base64UrlEncode(array);
|
|
55
|
+
}
|
|
56
|
+
function generateStateWithReturnUrl(returnUrl) {
|
|
57
|
+
const csrf = generateState();
|
|
58
|
+
const stateData = returnUrl ? { csrf, returnUrl } : { csrf };
|
|
59
|
+
const encoder = new TextEncoder;
|
|
60
|
+
const jsonBytes = encoder.encode(JSON.stringify(stateData));
|
|
61
|
+
return base64UrlEncode(jsonBytes);
|
|
62
|
+
}
|
|
63
|
+
function parseState(state) {
|
|
64
|
+
try {
|
|
65
|
+
const decoded = base64UrlDecode(state);
|
|
66
|
+
const parsed = JSON.parse(decoded);
|
|
67
|
+
if (typeof parsed === "string") {
|
|
68
|
+
return { csrf: parsed };
|
|
69
|
+
} else if (parsed && typeof parsed === "object") {
|
|
70
|
+
return {
|
|
71
|
+
csrf: parsed.csrf || state,
|
|
72
|
+
returnUrl: parsed.returnUrl
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { csrf: state };
|
|
76
|
+
} catch {
|
|
77
|
+
return { csrf: state };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function base64UrlEncode(array) {
|
|
81
|
+
let base64 = "";
|
|
82
|
+
if (typeof Buffer !== "undefined") {
|
|
83
|
+
base64 = Buffer.from(array).toString("base64");
|
|
84
|
+
} else {
|
|
85
|
+
const binary = String.fromCharCode(...array);
|
|
86
|
+
base64 = btoa(binary);
|
|
87
|
+
}
|
|
88
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
89
|
+
}
|
|
90
|
+
function base64UrlDecode(str) {
|
|
91
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
92
|
+
while (base64.length % 4 !== 0) {
|
|
93
|
+
base64 += "=";
|
|
94
|
+
}
|
|
95
|
+
if (typeof Buffer !== "undefined") {
|
|
96
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
97
|
+
} else {
|
|
98
|
+
const binary = atob(base64);
|
|
99
|
+
const bytes = new Uint8Array(binary.length);
|
|
100
|
+
for (let i = 0;i < binary.length; i++) {
|
|
101
|
+
bytes[i] = binary.charCodeAt(i);
|
|
102
|
+
}
|
|
103
|
+
const decoder = new TextDecoder;
|
|
104
|
+
return decoder.decode(bytes);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/adapters/base-handler.ts
|
|
109
|
+
var MCP_SERVER_URL = "https://mcp.integrate.dev/api/v1/mcp";
|
|
110
|
+
|
|
111
|
+
class OAuthHandler {
|
|
112
|
+
config;
|
|
113
|
+
serverUrl;
|
|
114
|
+
apiKey;
|
|
115
|
+
constructor(config) {
|
|
116
|
+
this.config = config;
|
|
117
|
+
if (!config || !config.providers) {
|
|
118
|
+
throw new Error("OAuthHandler requires a valid config with providers");
|
|
119
|
+
}
|
|
120
|
+
this.serverUrl = config.serverUrl || MCP_SERVER_URL;
|
|
121
|
+
this.apiKey = config.apiKey;
|
|
122
|
+
}
|
|
123
|
+
getHeaders(additionalHeaders) {
|
|
124
|
+
const headers = {
|
|
125
|
+
...additionalHeaders
|
|
126
|
+
};
|
|
127
|
+
if (this.apiKey) {
|
|
128
|
+
headers["X-API-KEY"] = this.apiKey;
|
|
129
|
+
}
|
|
130
|
+
return headers;
|
|
131
|
+
}
|
|
132
|
+
async handleAuthorize(request) {
|
|
133
|
+
const providerConfig = this.config.providers[request.provider];
|
|
134
|
+
if (!providerConfig) {
|
|
135
|
+
throw new Error(`Provider ${request.provider} not configured. Add OAuth credentials to your API route configuration.`);
|
|
136
|
+
}
|
|
137
|
+
if (!providerConfig.clientId || !providerConfig.clientSecret) {
|
|
138
|
+
throw new Error(`Missing OAuth credentials for ${request.provider}. Check your environment variables.`);
|
|
139
|
+
}
|
|
140
|
+
const url = new URL("/oauth/authorize", this.serverUrl);
|
|
141
|
+
url.searchParams.set("provider", request.provider);
|
|
142
|
+
url.searchParams.set("client_id", providerConfig.clientId);
|
|
143
|
+
url.searchParams.set("client_secret", providerConfig.clientSecret);
|
|
144
|
+
url.searchParams.set("scope", request.scopes.join(","));
|
|
145
|
+
url.searchParams.set("state", request.state);
|
|
146
|
+
url.searchParams.set("code_challenge", request.codeChallenge);
|
|
147
|
+
url.searchParams.set("code_challenge_method", request.codeChallengeMethod);
|
|
148
|
+
const redirectUri = request.redirectUri || providerConfig.redirectUri;
|
|
149
|
+
if (redirectUri) {
|
|
150
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
151
|
+
}
|
|
152
|
+
const response = await fetch(url.toString(), {
|
|
153
|
+
method: "GET",
|
|
154
|
+
headers: this.getHeaders()
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const error = await response.text();
|
|
158
|
+
throw new Error(`MCP server failed to generate authorization URL: ${error}`);
|
|
159
|
+
}
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
return data;
|
|
162
|
+
}
|
|
163
|
+
async handleCallback(request) {
|
|
164
|
+
const providerConfig = this.config.providers[request.provider];
|
|
165
|
+
if (!providerConfig) {
|
|
166
|
+
throw new Error(`Provider ${request.provider} not configured. Add OAuth credentials to your API route configuration.`);
|
|
167
|
+
}
|
|
168
|
+
if (!providerConfig.clientId || !providerConfig.clientSecret) {
|
|
169
|
+
throw new Error(`Missing OAuth credentials for ${request.provider}. Check your environment variables.`);
|
|
170
|
+
}
|
|
171
|
+
const url = new URL("/oauth/callback", this.serverUrl);
|
|
172
|
+
const response = await fetch(url.toString(), {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: this.getHeaders({
|
|
175
|
+
"Content-Type": "application/json"
|
|
176
|
+
}),
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
provider: request.provider,
|
|
179
|
+
code: request.code,
|
|
180
|
+
code_verifier: request.codeVerifier,
|
|
181
|
+
state: request.state,
|
|
182
|
+
client_id: providerConfig.clientId,
|
|
183
|
+
client_secret: providerConfig.clientSecret,
|
|
184
|
+
redirect_uri: providerConfig.redirectUri
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const error = await response.text();
|
|
189
|
+
throw new Error(`MCP server failed to exchange authorization code: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
194
|
+
async handleStatus(provider, accessToken) {
|
|
195
|
+
const url = new URL("/oauth/status", this.serverUrl);
|
|
196
|
+
url.searchParams.set("provider", provider);
|
|
197
|
+
const response = await fetch(url.toString(), {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers: this.getHeaders({
|
|
200
|
+
Authorization: `Bearer ${accessToken}`
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
if (response.status === 401) {
|
|
205
|
+
return {
|
|
206
|
+
authorized: false
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const error = await response.text();
|
|
210
|
+
throw new Error(`MCP server failed to check authorization status: ${error}`);
|
|
211
|
+
}
|
|
212
|
+
const data = await response.json();
|
|
213
|
+
return data;
|
|
214
|
+
}
|
|
215
|
+
async handleDisconnect(request, accessToken) {
|
|
216
|
+
if (!accessToken) {
|
|
217
|
+
throw new Error("No access token provided. Cannot disconnect provider.");
|
|
218
|
+
}
|
|
219
|
+
const url = new URL("/oauth/disconnect", this.serverUrl);
|
|
220
|
+
const response = await fetch(url.toString(), {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: this.getHeaders({
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
Authorization: `Bearer ${accessToken}`
|
|
225
|
+
}),
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
provider: request.provider
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const error = await response.text();
|
|
232
|
+
throw new Error(`MCP server failed to disconnect provider: ${error}`);
|
|
233
|
+
}
|
|
234
|
+
const data = await response.json();
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/adapters/nextjs.ts
|
|
240
|
+
function createNextOAuthHandler(config) {
|
|
241
|
+
const handler = new OAuthHandler(config);
|
|
242
|
+
const handlers = {
|
|
243
|
+
async authorize(req) {
|
|
244
|
+
try {
|
|
245
|
+
const body = await req.json();
|
|
246
|
+
const result = await handler.handleAuthorize(body);
|
|
247
|
+
return Response.json(result);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error("[OAuth Authorize] Error:", error);
|
|
250
|
+
return Response.json({ error: error.message || "Failed to get authorization URL" }, { status: 500 });
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
async callback(req) {
|
|
254
|
+
try {
|
|
255
|
+
const body = await req.json();
|
|
256
|
+
const result = await handler.handleCallback(body);
|
|
257
|
+
return Response.json(result);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error("[OAuth Callback] Error:", error);
|
|
260
|
+
return Response.json({ error: error.message || "Failed to exchange authorization code" }, { status: 500 });
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
async status(req) {
|
|
264
|
+
try {
|
|
265
|
+
const provider = req.nextUrl.searchParams.get("provider");
|
|
266
|
+
const authHeader = req.headers.get("authorization");
|
|
267
|
+
if (!provider) {
|
|
268
|
+
return Response.json({ error: "Missing provider query parameter" }, { status: 400 });
|
|
269
|
+
}
|
|
270
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
271
|
+
return Response.json({ error: "Missing or invalid Authorization header" }, { status: 400 });
|
|
272
|
+
}
|
|
273
|
+
const accessToken = authHeader.substring(7);
|
|
274
|
+
const result = await handler.handleStatus(provider, accessToken);
|
|
275
|
+
return Response.json(result);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error("[OAuth Status] Error:", error);
|
|
278
|
+
return Response.json({ error: error.message || "Failed to check authorization status" }, { status: 500 });
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
async disconnect(req) {
|
|
282
|
+
try {
|
|
283
|
+
const authHeader = req.headers.get("authorization");
|
|
284
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
285
|
+
return Response.json({ error: "Missing or invalid Authorization header" }, { status: 400 });
|
|
286
|
+
}
|
|
287
|
+
const accessToken = authHeader.substring(7);
|
|
288
|
+
const body = await req.json();
|
|
289
|
+
const { provider } = body;
|
|
290
|
+
if (!provider) {
|
|
291
|
+
return Response.json({ error: "Missing provider in request body" }, { status: 400 });
|
|
292
|
+
}
|
|
293
|
+
const result = await handler.handleDisconnect({ provider }, accessToken);
|
|
294
|
+
return Response.json(result);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error("[OAuth Disconnect] Error:", error);
|
|
297
|
+
return Response.json({ error: error.message || "Failed to disconnect provider" }, { status: 500 });
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
createRoutes() {
|
|
301
|
+
return {
|
|
302
|
+
async POST(req, context) {
|
|
303
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
304
|
+
const action = params.action;
|
|
305
|
+
if (action === "authorize") {
|
|
306
|
+
return handlers.authorize(req);
|
|
307
|
+
}
|
|
308
|
+
if (action === "callback") {
|
|
309
|
+
return handlers.callback(req);
|
|
310
|
+
}
|
|
311
|
+
if (action === "disconnect") {
|
|
312
|
+
return handlers.disconnect(req);
|
|
313
|
+
}
|
|
314
|
+
return Response.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
315
|
+
},
|
|
316
|
+
async GET(req, context) {
|
|
317
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
318
|
+
const action = params.action;
|
|
319
|
+
if (action === "status") {
|
|
320
|
+
return handlers.status(req);
|
|
321
|
+
}
|
|
322
|
+
return Response.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
toNextJsHandler(redirectConfig) {
|
|
327
|
+
const defaultRedirectUrl = redirectConfig?.redirectUrl || "/";
|
|
328
|
+
const errorRedirectUrl = redirectConfig?.errorRedirectUrl || "/auth-error";
|
|
329
|
+
return {
|
|
330
|
+
async POST(req, context) {
|
|
331
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
332
|
+
const segments = params.all || [];
|
|
333
|
+
if (segments.length === 2 && segments[0] === "oauth") {
|
|
334
|
+
const action = segments[1];
|
|
335
|
+
if (action === "authorize") {
|
|
336
|
+
return handlers.authorize(req);
|
|
337
|
+
}
|
|
338
|
+
if (action === "callback") {
|
|
339
|
+
return handlers.callback(req);
|
|
340
|
+
}
|
|
341
|
+
if (action === "disconnect") {
|
|
342
|
+
return handlers.disconnect(req);
|
|
343
|
+
}
|
|
344
|
+
return Response.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
345
|
+
}
|
|
346
|
+
return Response.json({ error: `Invalid route: /${segments.join("/")}` }, { status: 404 });
|
|
347
|
+
},
|
|
348
|
+
async GET(req, context) {
|
|
349
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
350
|
+
const segments = params.all || [];
|
|
351
|
+
if (segments.length === 2 && segments[0] === "oauth") {
|
|
352
|
+
const action = segments[1];
|
|
353
|
+
if (action === "status") {
|
|
354
|
+
return handlers.status(req);
|
|
355
|
+
}
|
|
356
|
+
if (action === "callback") {
|
|
357
|
+
const { searchParams } = new URL(req.url);
|
|
358
|
+
const code = searchParams.get("code");
|
|
359
|
+
const state = searchParams.get("state");
|
|
360
|
+
const error = searchParams.get("error");
|
|
361
|
+
const errorDescription = searchParams.get("error_description");
|
|
362
|
+
if (error) {
|
|
363
|
+
const errorMsg = errorDescription || error;
|
|
364
|
+
console.error("[OAuth Redirect] Error:", errorMsg);
|
|
365
|
+
return Response.redirect(new URL(`${errorRedirectUrl}?error=${encodeURIComponent(errorMsg)}`, req.url));
|
|
366
|
+
}
|
|
367
|
+
if (!code || !state) {
|
|
368
|
+
console.error("[OAuth Redirect] Missing code or state parameter");
|
|
369
|
+
return Response.redirect(new URL(`${errorRedirectUrl}?error=${encodeURIComponent("Invalid OAuth callback")}`, req.url));
|
|
370
|
+
}
|
|
371
|
+
let returnUrl = defaultRedirectUrl;
|
|
372
|
+
try {
|
|
373
|
+
const { parseState: parseState2 } = await Promise.resolve().then(() => exports_pkce);
|
|
374
|
+
const stateData = parseState2(state);
|
|
375
|
+
if (stateData.returnUrl) {
|
|
376
|
+
returnUrl = stateData.returnUrl;
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
try {
|
|
380
|
+
const referrer = req.headers?.get?.("referer") || req.headers?.get?.("referrer");
|
|
381
|
+
if (referrer) {
|
|
382
|
+
const referrerUrl = new URL(referrer);
|
|
383
|
+
const currentUrl = new URL(req.url);
|
|
384
|
+
if (referrerUrl.origin === currentUrl.origin) {
|
|
385
|
+
returnUrl = referrerUrl.pathname + referrerUrl.search;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch {}
|
|
389
|
+
}
|
|
390
|
+
const targetUrl = new URL(returnUrl, req.url);
|
|
391
|
+
targetUrl.hash = `oauth_callback=${encodeURIComponent(JSON.stringify({ code, state }))}`;
|
|
392
|
+
return Response.redirect(targetUrl);
|
|
393
|
+
}
|
|
394
|
+
return Response.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
395
|
+
}
|
|
396
|
+
return Response.json({ error: `Invalid route: /${segments.join("/")}` }, { status: 404 });
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
return handlers;
|
|
402
|
+
}
|
|
403
|
+
export {
|
|
404
|
+
createNextOAuthHandler
|
|
405
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/adapters/base-handler.ts
|
|
13
|
+
var MCP_SERVER_URL = "https://mcp.integrate.dev/api/v1/mcp";
|
|
14
|
+
|
|
15
|
+
class OAuthHandler {
|
|
16
|
+
config;
|
|
17
|
+
serverUrl;
|
|
18
|
+
apiKey;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
if (!config || !config.providers) {
|
|
22
|
+
throw new Error("OAuthHandler requires a valid config with providers");
|
|
23
|
+
}
|
|
24
|
+
this.serverUrl = config.serverUrl || MCP_SERVER_URL;
|
|
25
|
+
this.apiKey = config.apiKey;
|
|
26
|
+
}
|
|
27
|
+
getHeaders(additionalHeaders) {
|
|
28
|
+
const headers = {
|
|
29
|
+
...additionalHeaders
|
|
30
|
+
};
|
|
31
|
+
if (this.apiKey) {
|
|
32
|
+
headers["X-API-KEY"] = this.apiKey;
|
|
33
|
+
}
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
|
36
|
+
async handleAuthorize(request) {
|
|
37
|
+
const providerConfig = this.config.providers[request.provider];
|
|
38
|
+
if (!providerConfig) {
|
|
39
|
+
throw new Error(`Provider ${request.provider} not configured. Add OAuth credentials to your API route configuration.`);
|
|
40
|
+
}
|
|
41
|
+
if (!providerConfig.clientId || !providerConfig.clientSecret) {
|
|
42
|
+
throw new Error(`Missing OAuth credentials for ${request.provider}. Check your environment variables.`);
|
|
43
|
+
}
|
|
44
|
+
const url = new URL("/oauth/authorize", this.serverUrl);
|
|
45
|
+
url.searchParams.set("provider", request.provider);
|
|
46
|
+
url.searchParams.set("client_id", providerConfig.clientId);
|
|
47
|
+
url.searchParams.set("client_secret", providerConfig.clientSecret);
|
|
48
|
+
url.searchParams.set("scope", request.scopes.join(","));
|
|
49
|
+
url.searchParams.set("state", request.state);
|
|
50
|
+
url.searchParams.set("code_challenge", request.codeChallenge);
|
|
51
|
+
url.searchParams.set("code_challenge_method", request.codeChallengeMethod);
|
|
52
|
+
const redirectUri = request.redirectUri || providerConfig.redirectUri;
|
|
53
|
+
if (redirectUri) {
|
|
54
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(url.toString(), {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: this.getHeaders()
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const error = await response.text();
|
|
62
|
+
throw new Error(`MCP server failed to generate authorization URL: ${error}`);
|
|
63
|
+
}
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
async handleCallback(request) {
|
|
68
|
+
const providerConfig = this.config.providers[request.provider];
|
|
69
|
+
if (!providerConfig) {
|
|
70
|
+
throw new Error(`Provider ${request.provider} not configured. Add OAuth credentials to your API route configuration.`);
|
|
71
|
+
}
|
|
72
|
+
if (!providerConfig.clientId || !providerConfig.clientSecret) {
|
|
73
|
+
throw new Error(`Missing OAuth credentials for ${request.provider}. Check your environment variables.`);
|
|
74
|
+
}
|
|
75
|
+
const url = new URL("/oauth/callback", this.serverUrl);
|
|
76
|
+
const response = await fetch(url.toString(), {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: this.getHeaders({
|
|
79
|
+
"Content-Type": "application/json"
|
|
80
|
+
}),
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
provider: request.provider,
|
|
83
|
+
code: request.code,
|
|
84
|
+
code_verifier: request.codeVerifier,
|
|
85
|
+
state: request.state,
|
|
86
|
+
client_id: providerConfig.clientId,
|
|
87
|
+
client_secret: providerConfig.clientSecret,
|
|
88
|
+
redirect_uri: providerConfig.redirectUri
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const error = await response.text();
|
|
93
|
+
throw new Error(`MCP server failed to exchange authorization code: ${error}`);
|
|
94
|
+
}
|
|
95
|
+
const data = await response.json();
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
async handleStatus(provider, accessToken) {
|
|
99
|
+
const url = new URL("/oauth/status", this.serverUrl);
|
|
100
|
+
url.searchParams.set("provider", provider);
|
|
101
|
+
const response = await fetch(url.toString(), {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: this.getHeaders({
|
|
104
|
+
Authorization: `Bearer ${accessToken}`
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
if (response.status === 401) {
|
|
109
|
+
return {
|
|
110
|
+
authorized: false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const error = await response.text();
|
|
114
|
+
throw new Error(`MCP server failed to check authorization status: ${error}`);
|
|
115
|
+
}
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
async handleDisconnect(request, accessToken) {
|
|
120
|
+
if (!accessToken) {
|
|
121
|
+
throw new Error("No access token provided. Cannot disconnect provider.");
|
|
122
|
+
}
|
|
123
|
+
const url = new URL("/oauth/disconnect", this.serverUrl);
|
|
124
|
+
const response = await fetch(url.toString(), {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: this.getHeaders({
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
Authorization: `Bearer ${accessToken}`
|
|
129
|
+
}),
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
provider: request.provider
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const error = await response.text();
|
|
136
|
+
throw new Error(`MCP server failed to disconnect provider: ${error}`);
|
|
137
|
+
}
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/adapters/node.ts
|
|
144
|
+
function fromNodeHeaders(nodeHeaders) {
|
|
145
|
+
const webHeaders = new Headers;
|
|
146
|
+
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
147
|
+
if (value !== undefined) {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
value.forEach((v) => webHeaders.append(key, v));
|
|
150
|
+
} else {
|
|
151
|
+
webHeaders.set(key, value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return webHeaders;
|
|
156
|
+
}
|
|
157
|
+
async function toWebRequest(req) {
|
|
158
|
+
const protocol = req.socket.encrypted ? "https" : "http";
|
|
159
|
+
const host = req.headers.host || "localhost";
|
|
160
|
+
const url = `${protocol}://${host}${req.url}`;
|
|
161
|
+
const headers = fromNodeHeaders(req.headers);
|
|
162
|
+
let body;
|
|
163
|
+
if (req.method && ["POST", "PUT", "PATCH"].includes(req.method)) {
|
|
164
|
+
body = await new Promise((resolve, reject) => {
|
|
165
|
+
let data = "";
|
|
166
|
+
req.on("data", (chunk) => data += chunk);
|
|
167
|
+
req.on("end", () => resolve(data));
|
|
168
|
+
req.on("error", reject);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return new Request(url, {
|
|
172
|
+
method: req.method,
|
|
173
|
+
headers,
|
|
174
|
+
body: body || undefined
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function sendWebResponse(webRes, nodeRes) {
|
|
178
|
+
nodeRes.statusCode = webRes.status;
|
|
179
|
+
webRes.headers.forEach((value, key) => {
|
|
180
|
+
nodeRes.setHeader(key, value);
|
|
181
|
+
});
|
|
182
|
+
const body = await webRes.text();
|
|
183
|
+
nodeRes.end(body);
|
|
184
|
+
}
|
|
185
|
+
function toNodeHandler(config) {
|
|
186
|
+
const oauthHandler = new OAuthHandler(config);
|
|
187
|
+
return async (req, res) => {
|
|
188
|
+
try {
|
|
189
|
+
const webReq = await toWebRequest(req);
|
|
190
|
+
const url = new URL(webReq.url);
|
|
191
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
192
|
+
const action = segments[segments.length - 1];
|
|
193
|
+
let webRes;
|
|
194
|
+
if (req.method === "POST") {
|
|
195
|
+
if (action === "authorize") {
|
|
196
|
+
const body = await webReq.json();
|
|
197
|
+
const result = await oauthHandler.handleAuthorize(body);
|
|
198
|
+
webRes = new Response(JSON.stringify(result), {
|
|
199
|
+
status: 200,
|
|
200
|
+
headers: { "Content-Type": "application/json" }
|
|
201
|
+
});
|
|
202
|
+
} else if (action === "callback") {
|
|
203
|
+
const body = await webReq.json();
|
|
204
|
+
const result = await oauthHandler.handleCallback(body);
|
|
205
|
+
webRes = new Response(JSON.stringify(result), {
|
|
206
|
+
status: 200,
|
|
207
|
+
headers: { "Content-Type": "application/json" }
|
|
208
|
+
});
|
|
209
|
+
} else if (action === "disconnect") {
|
|
210
|
+
const authHeader = webReq.headers.get("authorization");
|
|
211
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
212
|
+
webRes = new Response(JSON.stringify({ error: "Missing or invalid Authorization header" }), {
|
|
213
|
+
status: 400,
|
|
214
|
+
headers: { "Content-Type": "application/json" }
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
const accessToken = authHeader.substring(7);
|
|
218
|
+
const body = await webReq.json();
|
|
219
|
+
const { provider } = body;
|
|
220
|
+
if (!provider) {
|
|
221
|
+
webRes = new Response(JSON.stringify({ error: "Missing provider in request body" }), {
|
|
222
|
+
status: 400,
|
|
223
|
+
headers: { "Content-Type": "application/json" }
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
const result = await oauthHandler.handleDisconnect({ provider }, accessToken);
|
|
227
|
+
webRes = new Response(JSON.stringify(result), {
|
|
228
|
+
status: 200,
|
|
229
|
+
headers: { "Content-Type": "application/json" }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
webRes = new Response(JSON.stringify({ error: `Unknown action: ${action}` }), {
|
|
235
|
+
status: 404,
|
|
236
|
+
headers: { "Content-Type": "application/json" }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
} else if (req.method === "GET" && action === "status") {
|
|
240
|
+
const provider = url.searchParams.get("provider");
|
|
241
|
+
const authHeader = webReq.headers.get("authorization");
|
|
242
|
+
if (!provider) {
|
|
243
|
+
webRes = new Response(JSON.stringify({ error: "Missing provider query parameter" }), {
|
|
244
|
+
status: 400,
|
|
245
|
+
headers: { "Content-Type": "application/json" }
|
|
246
|
+
});
|
|
247
|
+
} else if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
248
|
+
webRes = new Response(JSON.stringify({ error: "Missing or invalid Authorization header" }), {
|
|
249
|
+
status: 400,
|
|
250
|
+
headers: { "Content-Type": "application/json" }
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
const accessToken = authHeader.substring(7);
|
|
254
|
+
const result = await oauthHandler.handleStatus(provider, accessToken);
|
|
255
|
+
webRes = new Response(JSON.stringify(result), {
|
|
256
|
+
status: 200,
|
|
257
|
+
headers: { "Content-Type": "application/json" }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
webRes = new Response(JSON.stringify({ error: `Unknown action: ${action}` }), {
|
|
262
|
+
status: 404,
|
|
263
|
+
headers: { "Content-Type": "application/json" }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
await sendWebResponse(webRes, res);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error("[OAuth Handler] Error:", error);
|
|
269
|
+
const errorRes = new Response(JSON.stringify({ error: error.message || "Internal server error" }), {
|
|
270
|
+
status: 500,
|
|
271
|
+
headers: { "Content-Type": "application/json" }
|
|
272
|
+
});
|
|
273
|
+
await sendWebResponse(errorRes, res);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
toNodeHandler,
|
|
279
|
+
fromNodeHeaders
|
|
280
|
+
};
|