Unified · Split
| @@ -135,17 +135,6 @@ · 134 unmodified lines | ||
| 135 | 135 | "bun-types": "catalog:", |
| 136 | 136 | }, |
| 137 | 137 | }, |
| 138 | − "packages/auth/oauth2": { | |
| 139 | − "name": "@executor/auth-oauth2", | |
| 140 | − "version": "0.0.0", | |
| 141 | − "dependencies": { | |
| 142 | − "effect": "catalog:", | |
| 143 | − }, | |
| 144 | − "devDependencies": { | |
| 145 | − "@types/node": "catalog:", | |
| 146 | − "bun-types": "catalog:", | |
| 147 | − }, | |
| 148 | − }, | |
| 149 | 138 | "packages/clients/react": { |
| 150 | 139 | "name": "@executor/react", |
| 151 | 140 | "version": "0.0.0", |
| @@ -351,8 +340,6 @@ · 199 unmodified lines | ||
| 351 | 340 | "dependencies": { |
| 352 | 341 | "@effect/platform": "catalog:", |
| 353 | 342 | "@effect/platform-node": "catalog:", |
| 354 | − "@executor/auth-mcp-oauth": "workspace:*", | |
| 355 | − "@executor/auth-oauth2": "workspace:*", | |
| 356 | 343 | "@executor/codemode-core": "workspace:*", |
| 357 | 344 | "@executor/ir": "workspace:*", |
| 358 | 345 | "@executor/runtime-deno-subprocess": "workspace:*", |
| @@ -436,7 +423,6 @@ · 77 unmodified lines | ||
| 436 | 423 | "name": "@executor/source-core", |
| 437 | 424 | "version": "0.0.0", |
| 438 | 425 | "dependencies": { |
| 439 | − "@executor/auth-oauth2": "workspace:*", | |
| 440 | 426 | "@executor/codemode-core": "workspace:*", |
| 441 | 427 | "@executor/ir": "workspace:*", |
| 442 | 428 | "@modelcontextprotocol/sdk": "^1.26.0", |
| @@ -482,7 +468,6 @@ · 39 unmodified lines | ||
| 482 | 468 | "version": "0.0.0", |
| 483 | 469 | "dependencies": { |
| 484 | 470 | "@effect/platform": "catalog:", |
| 485 | − "@executor/auth-oauth2": "workspace:*", | |
| 486 | 471 | "@executor/codemode-core": "workspace:*", |
| 487 | 472 | "@executor/ir": "workspace:*", |
| 488 | 473 | "@executor/platform-sdk": "workspace:*", |
| @@ -900,8 +885,6 @@ · 411 unmodified lines | ||
| 900 | 885 | |
| 901 | 886 | "@executor/auth-mcp-oauth": ["@executor/auth-mcp-oauth@workspace:packages/auth/mcp-oauth"], |
| 902 | 887 | |
| 903 | − "@executor/auth-oauth2": ["@executor/auth-oauth2@workspace:packages/auth/oauth2"], | |
| 904 | − | |
| 905 | 888 | "@executor/codemode-core": ["@executor/codemode-core@workspace:packages/kernel/core"], |
| 906 | 889 | |
| 907 | 890 | "@executor/docs": ["@executor/docs@workspace:apps/docs"], |
| @@ -1,22 +0,0 @@ | ||
| 1 | −{ | |
| 2 | − "name": "@executor/auth-oauth2", | |
| 3 | − "private": true, | |
| 4 | − "type": "module", | |
| 5 | − "exports": { | |
| 6 | − ".": "./src/index.ts" | |
| 7 | − }, | |
| 8 | − "files": [ | |
| 9 | − "src" | |
| 10 | − ], | |
| 11 | − "scripts": { | |
| 12 | − "typecheck": "bunx tsc --noEmit -p tsconfig.json" | |
| 13 | − }, | |
| 14 | − "dependencies": { | |
| 15 | − "effect": "catalog:" | |
| 16 | − }, | |
| 17 | − "devDependencies": { | |
| 18 | − "@types/node": "catalog:", | |
| 19 | − "bun-types": "catalog:" | |
| 20 | − }, | |
| 21 | − "version": "0.0.0" | |
| 22 | −} | |
| @@ -1,19 +0,0 @@ | ||
| 1 | −{ | |
| 2 | − "compilerOptions": { | |
| 3 | − "target": "ES2022", | |
| 4 | − "module": "ESNext", | |
| 5 | − "moduleResolution": "Bundler", | |
| 6 | − "strict": true, | |
| 7 | − "skipLibCheck": true, | |
| 8 | − "noEmit": true, | |
| 9 | − "lib": ["ES2022"], | |
| 10 | − "types": ["bun-types", "node"] | |
| 11 | − }, | |
| 12 | − "include": [ | |
| 13 | − "src/**/*.ts", | |
| 14 | − "src/**/*.d.ts" | |
| 15 | − ], | |
| 16 | − "exclude": [ | |
| 17 | − "src/**/*.test.ts" | |
| 18 | − ] | |
| 19 | −} | |
| @@ -27,8 +27,6 @@ · 26 unmodified lines | ||
| 27 | 27 | "dependencies": { |
| 28 | 28 | "@effect/platform": "catalog:", |
| 29 | 29 | "@effect/platform-node": "catalog:", |
| 30 | − "@executor/auth-mcp-oauth": "workspace:*", | |
| 31 | − "@executor/auth-oauth2": "workspace:*", | |
| 32 | 30 | "@executor/codemode-core": "workspace:*", |
| 33 | 31 | "@executor/ir": "workspace:*", |
| 34 | 32 | "@executor/runtime-deno-subprocess": "workspace:*", |
| @@ -1 +0,0 @@ | ||
| 1 | −export * from "@executor/auth-mcp-oauth"; | |
| @@ -1,110 +0,0 @@ | ||
| 1 | −import { | |
| 2 | − createServer, | |
| 3 | −} from "node:http"; | |
| 4 | − | |
| 5 | −import { | |
| 6 | − describe, | |
| 7 | − expect, | |
| 8 | − it, | |
| 9 | −} from "@effect/vitest"; | |
| 10 | −import * as Effect from "effect/Effect"; | |
| 11 | − | |
| 12 | −import { | |
| 13 | − startOauthLoopbackRedirectServer, | |
| 14 | −} from "./oauth-loopback"; | |
| 15 | − | |
| 16 | −const toError = (cause: unknown): Error => | |
| 17 | − cause instanceof Error ? cause : new Error(String(cause)); | |
| 18 | − | |
| 19 | −const makeCompletionServer = Effect.acquireRelease( | |
| 20 | − Effect.tryPromise({ | |
| 21 | − try: () => | |
| 22 | − new Promise<{ | |
| 23 | − completionUrl: string; | |
| 24 | − requests: string[]; | |
| 25 | − server: ReturnType<typeof createServer>; | |
| 26 | − }>((resolve, reject) => { | |
| 27 | − const requests: string[] = []; | |
| 28 | − const server = createServer((request, response) => { | |
| 29 | − requests.push(request.url ?? "/"); | |
| 30 | − response.statusCode = 200; | |
| 31 | − response.end("ok"); | |
| 32 | − }); | |
| 33 | − | |
| 34 | − server.listen(0, "127.0.0.1", (error?: Error) => { | |
| 35 | − if (error) { | |
| 36 | − reject(error); | |
| 37 | − return; | |
| 38 | − } | |
| 39 | − | |
| 40 | − const address = server.address(); | |
| 41 | − if (!address || typeof address === "string") { | |
| 42 | − reject(new Error("Failed to resolve completion server address")); | |
| 43 | − return; | |
| 44 | − } | |
| 45 | − | |
| 46 | − resolve({ | |
| 47 | − completionUrl: `http://127.0.0.1:${address.port}/oauth/complete?sessionId=test-session`, | |
| 48 | − requests, | |
| 49 | − server, | |
| 50 | − }); | |
| 51 | − }); | |
| 52 | − }), | |
| 53 | − catch: toError, | |
| 54 | − }), | |
| 55 | − ({ server }) => | |
| 56 | − Effect.tryPromise({ | |
| 57 | − try: () => | |
| 58 | − new Promise<void>((resolve, reject) => { | |
| 59 | − server.close((error) => { | |
| 60 | − if (error) { | |
| 61 | − reject(error); | |
| 62 | − return; | |
| 63 | − } | |
| 64 | − resolve(); | |
| 65 | − }); | |
| 66 | − }), | |
| 67 | − catch: toError, | |
| 68 | − }), | |
| 69 | −); | |
| 70 | − | |
| 71 | −const withCompletionServer = <T>( | |
| 72 | − handler: (input: { | |
| 73 | − completionUrl: string; | |
| 74 | − requests: string[]; | |
| 75 | − }) => Effect.Effect<T, Error, never>, | |
| 76 | −) => Effect.flatMap(makeCompletionServer, handler); | |
| 77 | − | |
| 78 | −describe("oauth-loopback", () => { | |
| 79 | − it.scoped( | |
| 80 | − "redirects loopback callbacks to the app completion URL with query params intact", | |
| 81 | − () => | |
| 82 | − withCompletionServer(({ completionUrl, requests }) => | |
| 83 | − Effect.gen(function* () { | |
| 84 | − const receiver = yield* Effect.acquireRelease( | |
| 85 | − startOauthLoopbackRedirectServer({ | |
| 86 | − completionUrl, | |
| 87 | − }), | |
| 88 | − (server) => server.close, | |
| 89 | − ); | |
| 90 | − | |
| 91 | − const response = yield* Effect.tryPromise({ | |
| 92 | − try: () => | |
| 93 | − fetch( | |
| 94 | − `${receiver.redirectUri}?state=oauth-state&code=oauth-code`, | |
| 95 | − { | |
| 96 | − redirect: "follow", | |
| 97 | − signal: AbortSignal.timeout(10_000), | |
| 98 | − }, | |
| 99 | − ), | |
| 100 | − catch: toError, | |
| 101 | − }); | |
| 102 | − | |
| 103 | − expect(response.ok).toBe(true); | |
| 104 | − expect(requests).toEqual([ | |
| 105 | − "/oauth/complete?sessionId=test-session&state=oauth-state&code=oauth-code", | |
| 106 | − ]); | |
| 107 | − }), | |
| 108 | − ), | |
| 109 | − ); | |
| 110 | −}); | |
| @@ -1,106 +0,0 @@ | ||
| 1 | −import { | |
| 2 | − createServer, | |
| 3 | −} from "node:http"; | |
| 4 | − | |
| 5 | −import * as Effect from "effect/Effect"; | |
| 6 | − | |
| 7 | −const DEFAULT_LOOPBACK_TIMEOUT_MS = 10 * 60_000; | |
| 8 | − | |
| 9 | −export type OauthLoopbackRedirectServer = { | |
| 10 | − redirectUri: string; | |
| 11 | − close: Effect.Effect<void, Error, never>; | |
| 12 | −}; | |
| 13 | − | |
| 14 | −const closeServer = ( | |
| 15 | − server: ReturnType<typeof createServer>, | |
| 16 | −): Promise<void> => | |
| 17 | − new Promise<void>((resolve, reject) => { | |
| 18 | − server.close((error) => { | |
| 19 | − if (error) { | |
| 20 | − reject(error); | |
| 21 | − return; | |
| 22 | − } | |
| 23 | − resolve(); | |
| 24 | − }); | |
| 25 | − }); | |
| 26 | − | |
| 27 | −export const startOauthLoopbackRedirectServer = (input: { | |
| 28 | − completionUrl: string; | |
| 29 | − publicHost?: "localhost" | "127.0.0.1"; | |
| 30 | − listenHost?: "127.0.0.1"; | |
| 31 | − timeoutMs?: number; | |
| 32 | −}): Effect.Effect<OauthLoopbackRedirectServer, Error, never> => | |
| 33 | − Effect.tryPromise({ | |
| 34 | − try: () => | |
| 35 | − new Promise<OauthLoopbackRedirectServer>((resolve, reject) => { | |
| 36 | − const publicHost = input.publicHost ?? "127.0.0.1"; | |
| 37 | − const listenHost = input.listenHost ?? "127.0.0.1"; | |
| 38 | − const completionUrl = new URL(input.completionUrl); | |
| 39 | − const server = createServer((request, response) => { | |
| 40 | − try { | |
| 41 | − const requestUrl = new URL( | |
| 42 | − request.url ?? "/", | |
| 43 | − `http://${publicHost}`, | |
| 44 | − ); | |
| 45 | − const redirectUrl = new URL(completionUrl.toString()); | |
| 46 | − | |
| 47 | − for (const [key, value] of requestUrl.searchParams.entries()) { | |
| 48 | − redirectUrl.searchParams.set(key, value); | |
| 49 | − } | |
| 50 | − | |
| 51 | − response.statusCode = 302; | |
| 52 | − response.setHeader("cache-control", "no-store"); | |
| 53 | − response.setHeader("location", redirectUrl.toString()); | |
| 54 | − response.end(); | |
| 55 | − } catch (cause) { | |
| 56 | − const error = cause instanceof Error ? cause : new Error(String(cause)); | |
| 57 | − response.statusCode = 500; | |
| 58 | − response.setHeader("content-type", "text/plain; charset=utf-8"); | |
| 59 | − response.end(`OAuth redirect failed: ${error.message}`); | |
| 60 | − } finally { | |
| 61 | − void closeServer(server).catch(() => {}); | |
| 62 | − } | |
| 63 | − }); | |
| 64 | − | |
| 65 | − let timeout: NodeJS.Timeout | null = null; | |
| 66 | − const cleanup = (): Promise<void> => { | |
| 67 | − if (timeout !== null) { | |
| 68 | − clearTimeout(timeout); | |
| 69 | − timeout = null; | |
| 70 | − } | |
| 71 | − return closeServer(server).catch(() => undefined); | |
| 72 | − }; | |
| 73 | − | |
| 74 | − server.once("error", (cause) => { | |
| 75 | − reject( | |
| 76 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 77 | − ); | |
| 78 | − }); | |
| 79 | − | |
| 80 | − server.listen(0, listenHost, () => { | |
| 81 | − const address = server.address(); | |
| 82 | − if (!address || typeof address === "string") { | |
| 83 | − reject(new Error("Failed to resolve OAuth loopback port")); | |
| 84 | − return; | |
| 85 | − } | |
| 86 | − | |
| 87 | − timeout = setTimeout(() => { | |
| 88 | − void cleanup(); | |
| 89 | − }, input.timeoutMs ?? DEFAULT_LOOPBACK_TIMEOUT_MS); | |
| 90 | − if (typeof timeout.unref === "function") { | |
| 91 | − timeout.unref(); | |
| 92 | − } | |
| 93 | − | |
| 94 | − resolve({ | |
| 95 | − redirectUri: `http://${publicHost}:${address.port}`, | |
| 96 | − close: Effect.tryPromise({ | |
| 97 | − try: cleanup, | |
| 98 | − catch: (cause) => | |
| 99 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 100 | − }), | |
| 101 | − }); | |
| 102 | − }); | |
| 103 | − }), | |
| 104 | − catch: (cause) => | |
| 105 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 106 | − }); | |
| @@ -1 +0,0 @@ | ||
| 1 | −export * from "@executor/auth-oauth2"; | |
| @@ -12,7 +12,6 @@ · 11 unmodified lines | ||
| 12 | 12 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 13 | 13 | }, |
| 14 | 14 | "dependencies": { |
| 15 | − "@executor/auth-oauth2": "workspace:*", | |
| 16 | 15 | "@executor/codemode-core": "workspace:*", |
| 17 | 16 | "@executor/ir": "workspace:*", |
| 18 | 17 | "@modelcontextprotocol/sdk": "^1.26.0", |
| @@ -1,12 +1,6 @@ | ||
| 1 | 1 | import * as Effect from "effect/Effect"; |
| 2 | 2 | import * as Schema from "effect/Schema"; |
| 3 | 3 | |
| 4 | −import { | |
| 5 | − buildOAuth2AuthorizationUrl, | |
| 6 | − createPkceCodeVerifier, | |
| 7 | − exchangeOAuth2AuthorizationCode, | |
| 8 | − refreshOAuth2AccessToken, | |
| 9 | −} from "@executor/auth-oauth2"; | |
| 10 | 4 | import { |
| 11 | 5 | createCatalogImportMetadata, |
| 12 | 6 | createSourceCatalogSyncResult, |
| @@ -58,6 +52,12 @@ import { · 45 unmodified lines | ||
| 58 | 52 | type GoogleDiscoveryToolManifest, |
| 59 | 53 | type GoogleDiscoveryToolProviderData, |
| 60 | 54 | } from "./types"; |
| 55 | +import { | |
| 56 | + buildOAuth2AuthorizationUrl, | |
| 57 | + createPkceCodeVerifier, | |
| 58 | + exchangeOAuth2AuthorizationCode, | |
| 59 | + refreshOAuth2AccessToken, | |
| 60 | +} from "./oauth2"; | |
| 61 | 61 | |
| 62 | 62 | const decodeStoredSourceData = Schema.decodeUnknownSync( |
| 63 | 63 | GoogleDiscoveryStoredSourceDataSchema, |
| @@ -42,7 +42,10 @@ export const buildOAuth2AuthorizationUrl = (input: { · 41 unmodified lines | ||
| 42 | 42 | url.searchParams.set("scope", input.scopes.join(" ")); |
| 43 | 43 | url.searchParams.set("state", input.state); |
| 44 | 44 | url.searchParams.set("code_challenge_method", "S256"); |
| 45 | − url.searchParams.set("code_challenge", createPkceCodeChallenge(input.codeVerifier)); | |
| 45 | + url.searchParams.set( | |
| 46 | + "code_challenge", | |
| 47 | + createPkceCodeChallenge(input.codeVerifier), | |
| 48 | + ); | |
| 46 | 49 | |
| 47 | 50 | for (const [key, value] of Object.entries(input.extraParams ?? {})) { |
| 48 | 51 | url.searchParams.set(key, value); |
| @@ -51,17 +54,23 @@ export const buildOAuth2AuthorizationUrl = (input: { · 2 unmodified lines | ||
| 51 | 54 | return url.toString(); |
| 52 | 55 | }; |
| 53 | 56 | |
| 54 | −const parseOAuth2TokenResponse = async (response: Response): Promise<OAuth2TokenResponse> => { | |
| 57 | +const parseOAuth2TokenResponse = async ( | |
| 58 | + response: Response, | |
| 59 | +): Promise<OAuth2TokenResponse> => { | |
| 55 | 60 | const rawText = await response.text(); |
| 56 | 61 | let parsed: unknown; |
| 57 | 62 | try { |
| 58 | 63 | parsed = JSON.parse(rawText); |
| 59 | 64 | } catch { |
| 60 | − throw new Error(`OAuth token endpoint returned non-JSON response (${response.status})`); | |
| 65 | + throw new Error( | |
| 66 | + `OAuth token endpoint returned non-JSON response (${response.status})`, | |
| 67 | + ); | |
| 61 | 68 | } |
| 62 | 69 | |
| 63 | 70 | if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { |
| 64 | − throw new Error(`OAuth token endpoint returned invalid JSON payload (${response.status})`); | |
| 71 | + throw new Error( | |
| 72 | + `OAuth token endpoint returned invalid JSON payload (${response.status})`, | |
| 73 | + ); | |
| 65 | 74 | } |
| 66 | 75 | |
| 67 | 76 | const record = parsed as Record<string, unknown>; |
| @@ -72,7 +81,8 @@ const parseOAuth2TokenResponse = async (response: Response): Promise<OAuth2Token · 4 unmodified lines | ||
| 72 | 81 | |
| 73 | 82 | if (!response.ok) { |
| 74 | 83 | const description = |
| 75 | − typeof record.error_description === "string" && record.error_description.length > 0 | |
| 84 | + typeof record.error_description === "string" | |
| 85 | + && record.error_description.length > 0 | |
| 76 | 86 | ? record.error_description |
| 77 | 87 | : typeof record.error === "string" && record.error.length > 0 |
| 78 | 88 | ? record.error |
| @@ -86,8 +96,12 @@ const parseOAuth2TokenResponse = async (response: Response): Promise<OAuth2Token · 7 unmodified lines | ||
| 86 | 96 | |
| 87 | 97 | return { |
| 88 | 98 | access_token: accessToken, |
| 89 | − token_type: typeof record.token_type === "string" ? record.token_type : undefined, | |
| 90 | − refresh_token: typeof record.refresh_token === "string" ? record.refresh_token : undefined, | |
| 99 | + token_type: | |
| 100 | + typeof record.token_type === "string" ? record.token_type : undefined, | |
| 101 | + refresh_token: | |
| 102 | + typeof record.refresh_token === "string" | |
| 103 | + ? record.refresh_token | |
| 104 | + : undefined, | |
| 91 | 105 | expires_in: typeof record.expires_in === "number" |
| 92 | 106 | ? record.expires_in |
| 93 | 107 | : typeof record.expires_in === "string" |
| @@ -137,7 +151,10 @@ export const exchangeOAuth2AuthorizationCode = (input: { · 43 unmodified lines | ||
| 137 | 151 | code: input.code, |
| 138 | 152 | }); |
| 139 | 153 | |
| 140 | − if (input.clientAuthentication === "client_secret_post" && input.clientSecret) { | |
| 154 | + if ( | |
| 155 | + input.clientAuthentication === "client_secret_post" | |
| 156 | + && input.clientSecret | |
| 157 | + ) { | |
| 141 | 158 | body.set("client_secret", input.clientSecret); |
| 142 | 159 | } |
| 143 | 160 | |
| @@ -162,7 +179,10 @@ export const refreshOAuth2AccessToken = (input: { · 18 unmodified lines | ||
| 162 | 179 | refresh_token: input.refreshToken, |
| 163 | 180 | }); |
| 164 | 181 | |
| 165 | − if (input.clientAuthentication === "client_secret_post" && input.clientSecret) { | |
| 182 | + if ( | |
| 183 | + input.clientAuthentication === "client_secret_post" | |
| 184 | + && input.clientSecret | |
| 185 | + ) { | |
| 166 | 186 | body.set("client_secret", input.clientSecret); |
| 167 | 187 | } |
| 168 | 188 | |
| @@ -13,7 +13,6 @@ · 12 unmodified lines | ||
| 13 | 13 | }, |
| 14 | 14 | "dependencies": { |
| 15 | 15 | "@effect/platform": "catalog:", |
| 16 | − "@executor/auth-oauth2": "workspace:*", | |
| 17 | 16 | "@executor/codemode-core": "workspace:*", |
| 18 | 17 | "@executor/ir": "workspace:*", |
| 19 | 18 | "@executor/platform-sdk": "workspace:*", |