- Introduced the new package `@executor/source-builtins` with dependencies on core source packages. - Updated `bun.lock` and `package.json` to include `@executor/source-builtins`. - Enhanced source discovery logic to utilize built-in source adapters for improved detection. - Refactored source adapters to streamline the integration of local configuration binding schemas. - Improved error handling and type definitions across various source-related modules.
Unified · Split
| @@ -271,6 +271,7 @@ · 270 unmodified lines | |||
| 271 | "@executor/runtime-deno-subprocess": "workspace:*", | 271 | "@executor/runtime-deno-subprocess": "workspace:*", |
| 272 | "@executor/runtime-quickjs": "workspace:*", | 272 | "@executor/runtime-quickjs": "workspace:*", |
| 273 | "@executor/runtime-ses": "workspace:*", | 273 | "@executor/runtime-ses": "workspace:*", |
| 274 | "@executor/source-builtins": "workspace:*", | ||
| 274 | "@executor/source-core": "workspace:*", | 275 | "@executor/source-core": "workspace:*", |
| 275 | "@executor/source-google-discovery": "workspace:*", | 276 | "@executor/source-google-discovery": "workspace:*", |
| 276 | "@executor/source-graphql": "workspace:*", | 277 | "@executor/source-graphql": "workspace:*", |
| @@ -316,6 +317,21 @@ · 39 unmodified lines | |||
| 316 | "bun-types": "catalog:", | 317 | "bun-types": "catalog:", |
| 317 | }, | 318 | }, |
| 318 | }, | 319 | }, |
| 320 | "packages/sources/builtins": { | ||
| 321 | "name": "@executor/source-builtins", | ||
| 322 | "version": "0.0.0", | ||
| 323 | "dependencies": { | ||
| 324 | "@executor/source-core": "workspace:*", | ||
| 325 | "@executor/source-google-discovery": "workspace:*", | ||
| 326 | "@executor/source-graphql": "workspace:*", | ||
| 327 | "@executor/source-mcp": "workspace:*", | ||
| 328 | "@executor/source-openapi": "workspace:*", | ||
| 329 | }, | ||
| 330 | "devDependencies": { | ||
| 331 | "@types/node": "catalog:", | ||
| 332 | "bun-types": "catalog:", | ||
| 333 | }, | ||
| 334 | }, | ||
| 319 | "packages/sources/core": { | 335 | "packages/sources/core": { |
| 320 | "name": "@executor/source-core", | 336 | "name": "@executor/source-core", |
| 321 | "version": "0.0.0", | 337 | "version": "0.0.0", |
| @@ -636,6 +652,8 @@ · 314 unmodified lines | |||
| 636 | 652 | ||
| 637 | "@executor/server": ["@executor/server@workspace:packages/platform/server"], | 653 | "@executor/server": ["@executor/server@workspace:packages/platform/server"], |
| 638 | 654 | ||
| 655 | "@executor/source-builtins": ["@executor/source-builtins@workspace:packages/sources/builtins"], | ||
| 656 | |||
| 639 | "@executor/source-core": ["@executor/source-core@workspace:packages/sources/core"], | 657 | "@executor/source-core": ["@executor/source-core@workspace:packages/sources/core"], |
| 640 | 658 | ||
| 641 | "@executor/source-google-discovery": ["@executor/source-google-discovery@workspace:packages/sources/google-discovery"], | 659 | "@executor/source-google-discovery": ["@executor/source-google-discovery@workspace:packages/sources/google-discovery"], |
| @@ -21,6 +21,7 @@ · 20 unmodified lines | |||
| 21 | "dependencies": { | 21 | "dependencies": { |
| 22 | "@effect/platform": "catalog:", | 22 | "@effect/platform": "catalog:", |
| 23 | "@executor/source-core": "workspace:*", | 23 | "@executor/source-core": "workspace:*", |
| 24 | "@executor/source-builtins": "workspace:*", | ||
| 24 | "@executor/auth-mcp-oauth": "workspace:*", | 25 | "@executor/auth-mcp-oauth": "workspace:*", |
| 25 | "@executor/auth-oauth2": "workspace:*", | 26 | "@executor/auth-oauth2": "workspace:*", |
| 26 | "@executor/codemode-core": "workspace:*", | 27 | "@executor/codemode-core": "workspace:*", |
| @@ -25,7 +25,10 @@ import { · 24 unmodified lines | |||
| 25 | } from "../../runtime/local/operations"; | 25 | } from "../../runtime/local/operations"; |
| 26 | import { discoverSource } from "../../runtime/sources/source-discovery"; | 26 | import { discoverSource } from "../../runtime/sources/source-discovery"; |
| 27 | import { sourceAdapterRequiresInteractiveConnect } from "../../runtime/sources/source-adapters"; | 27 | import { sourceAdapterRequiresInteractiveConnect } from "../../runtime/sources/source-adapters"; |
| 28 | import { RuntimeSourceAuthServiceTag } from "../../runtime/sources/source-auth-service"; | 28 | import { |
| 29 | RuntimeSourceAuthServiceTag, | ||
| 30 | type ExecutorAddSourceInput, | ||
| 31 | } from "../../runtime/sources/source-auth-service"; | ||
| 29 | 32 | ||
| 30 | import { | 33 | import { |
| 31 | ControlPlaneBadRequestError, | 34 | ControlPlaneBadRequestError, |
| @@ -33,6 +36,7 @@ import { · 1 unmodified line | |||
| 33 | ControlPlaneStorageError, | 36 | ControlPlaneStorageError, |
| 34 | } from "../errors"; | 37 | } from "../errors"; |
| 35 | import { ControlPlaneApi } from "../api"; | 38 | import { ControlPlaneApi } from "../api"; |
| 39 | import type { ConnectSourcePayload } from "./api"; | ||
| 36 | import { resolveRequestedLocalWorkspace } from "../local-context"; | 40 | import { resolveRequestedLocalWorkspace } from "../local-context"; |
| 37 | 41 | ||
| 38 | const readHeader = (headers: unknown, name: string): string | null => { | 42 | const readHeader = (headers: unknown, name: string): string | null => { |
| @@ -79,6 +83,11 @@ const toBadRequestError = (operation: string, cause: unknown) => { · 40 unmodified lines | |||
| 79 | }); | 83 | }); |
| 80 | }; | 84 | }; |
| 81 | 85 | ||
| 86 | const isInteractiveConnectPayload = ( | ||
| 87 | payload: ConnectSourcePayload, | ||
| 88 | ): payload is Extract<ConnectSourcePayload, { kind?: "mcp" }> => | ||
| 89 | payload.kind === undefined || sourceAdapterRequiresInteractiveConnect(payload.kind); | ||
| 90 | |||
| 82 | const escapeHtml = (value: string): string => | 91 | const escapeHtml = (value: string): string => |
| 83 | value | 92 | value |
| 84 | .replaceAll("&", "&") | 93 | .replaceAll("&", "&") |
| @@ -677,8 +686,8 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 592 unmodified lines | |||
| 677 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 686 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 678 | const baseUrl = resolveRequestOrigin(request); | 687 | const baseUrl = resolveRequestOrigin(request); |
| 679 | 688 | ||
| 680 | if (sourceAdapterRequiresInteractiveConnect(payload.kind)) { | 689 | if (isInteractiveConnectPayload(payload)) { |
| 681 | return yield* sourceAuthService.connectMcpSource({ | 690 | return yield* sourceAuthService.connectMcpSource({ |
| 682 | workspaceId: path.workspaceId, | 691 | workspaceId: path.workspaceId, |
| 683 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | 692 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 684 | endpoint: payload.endpoint, | 693 | endpoint: payload.endpoint, |
| @@ -695,16 +704,16 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 10 unmodified lines | |||
| 695 | }); | 704 | }); |
| 696 | } | 705 | } |
| 697 | 706 | ||
| 698 | return yield* sourceAuthService.addExecutorSource( | 707 | return yield* sourceAuthService.addExecutorSource( |
| 699 | { | 708 | ({ |
| 700 | workspaceId: path.workspaceId, | 709 | workspaceId: path.workspaceId, |
| 701 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | 710 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 702 | executionId: null, | 711 | executionId: null, |
| 703 | interactionId: null, | 712 | interactionId: null, |
| 704 | ...payload, | 713 | ...(payload as Record<string, unknown>), |
| 705 | }, | 714 | } as ExecutorAddSourceInput), |
| 706 | { baseUrl }, | 715 | { baseUrl }, |
| 707 | ); | 716 | ); |
| 708 | }).pipe( | 717 | }).pipe( |
| 709 | Effect.catchAll((cause) => | 718 | Effect.catchAll((cause) => |
| 710 | Effect.fail(toBadRequestError("sources.connect", cause)), | 719 | Effect.fail(toBadRequestError("sources.connect", cause)), |
| @@ -6,7 +6,7 @@ import { · 5 unmodified lines | |||
| 6 | buildGraphqlToolPresentation, | 6 | buildGraphqlToolPresentation, |
| 7 | compileGraphqlToolDefinitions, | 7 | compileGraphqlToolDefinitions, |
| 8 | type GraphqlToolManifest, | 8 | type GraphqlToolManifest, |
| 9 | } from "../../sources/graphql-tools"; | 9 | } from "@executor/source-graphql"; |
| 10 | import { createGoogleDiscoveryCatalogSnapshot } from "@executor/source-google-discovery"; | 10 | import { createGoogleDiscoveryCatalogSnapshot } from "@executor/source-google-discovery"; |
| 11 | import { createGraphqlCatalogSnapshot } from "@executor/source-graphql"; | 11 | import { createGraphqlCatalogSnapshot } from "@executor/source-graphql"; |
| 12 | import { createMcpCatalogSnapshot } from "@executor/source-mcp"; | 12 | import { createMcpCatalogSnapshot } from "@executor/source-mcp"; |
| @@ -29,9 +29,9 @@ import { · 28 unmodified lines | |||
| 29 | } from "../../sources/source-adapters"; | 29 | } from "../../sources/source-adapters"; |
| 30 | import { | 30 | import { |
| 31 | catalogSyncResultFromMcpManifest, | 31 | catalogSyncResultFromMcpManifest, |
| 32 | } from "../../sources/source-adapters/mcp"; | 32 | } from "@executor/source-mcp"; |
| 33 | import { SecretMaterialResolverService } from "../../local/secret-material-providers"; | 33 | import { SecretMaterialResolverService } from "../../local/secret-material-providers"; |
| 34 | import { snapshotFromSourceCatalogSyncResult } from "../../sources/catalog-sync-result"; | 34 | import { snapshotFromSourceCatalogSyncResult } from "@executor/source-core"; |
| 35 | import { | 35 | import { |
| 36 | refreshSourceTypeDeclarationInBackground, | 36 | refreshSourceTypeDeclarationInBackground, |
| 37 | } from "./type-declarations"; | 37 | } from "./type-declarations"; |
| @@ -21,6 +21,8 @@ import { · 20 unmodified lines | |||
| 21 | WorkspaceOauthClientIdSchema, | 21 | WorkspaceOauthClientIdSchema, |
| 22 | } from "#schema"; | 22 | } from "#schema"; |
| 23 | import type { ToolPath } from "@executor/codemode-core"; | 23 | import type { ToolPath } from "@executor/codemode-core"; |
| 24 | import { createCatalogImportMetadata } from "@executor/source-core"; | ||
| 25 | import { createGraphqlCatalogFragment } from "@executor/source-graphql"; | ||
| 24 | 26 | ||
| 25 | import { | 27 | import { |
| 26 | type ControlPlaneRuntime, | 28 | type ControlPlaneRuntime, |
| @@ -33,6 +35,14 @@ import { decodeSourceCredentialSelectionContent } from "./sources/source-credent · 6 unmodified lines | |||
| 33 | import { persistSource } from "./sources/source-store"; | 35 | import { persistSource } from "./sources/source-store"; |
| 34 | import { withControlPlaneClient } from "./execution/test-http-client"; | 36 | import { withControlPlaneClient } from "./execution/test-http-client"; |
| 35 | import { runtimeEffectError } from "./effect-errors"; | 37 | import { runtimeEffectError } from "./effect-errors"; |
| 38 | import { resolveLocalWorkspaceContext } from "./local/config"; | ||
| 39 | import { writeLocalControlPlaneState } from "./local/control-plane-store"; | ||
| 40 | import { deriveLocalInstallation } from "./local/installation"; | ||
| 41 | import { | ||
| 42 | buildLocalSourceArtifact, | ||
| 43 | writeLocalSourceArtifact, | ||
| 44 | } from "./local/source-artifacts"; | ||
| 45 | import { writeLocalWorkspaceState } from "./local/workspace-state"; | ||
| 36 | 46 | ||
| 37 | const makeRuntime = Effect.gen(function* () { | 47 | const makeRuntime = Effect.gen(function* () { |
| 38 | const fs = yield* FileSystem.FileSystem; | 48 | const fs = yield* FileSystem.FileSystem; |
| @@ -699,6 +709,188 @@ describe("control-plane-runtime", () => { · 660 unmodified lines | |||
| 699 | }), | 709 | }), |
| 700 | ); | 710 | ); |
| 701 | 711 | ||
| 712 | it.scoped("loads a v1.2.3-style workspace artifact on startup", () => | ||
| 713 | Effect.gen(function* () { | ||
| 714 | const fs = yield* FileSystem.FileSystem; | ||
| 715 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 716 | prefix: "executor-control-plane-runtime-v123-", | ||
| 717 | }); | ||
| 718 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 719 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 720 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 721 | const configPath = join(configDirectory, "executor.jsonc"); | ||
| 722 | const sourceId = SourceIdSchema.make("graphql"); | ||
| 723 | const now = Date.now(); | ||
| 724 | |||
| 725 | yield* fs.makeDirectory(configDirectory, { recursive: true }); | ||
| 726 | yield* fs.writeFileString( | ||
| 727 | configPath, | ||
| 728 | `{ | ||
| 729 | "sources": { | ||
| 730 | "graphql": { | ||
| 731 | "kind": "graphql", | ||
| 732 | "name": "GraphQL API", | ||
| 733 | "namespace": "graphql", | ||
| 734 | "connection": { | ||
| 735 | "endpoint": "https://example.com/graphql" | ||
| 736 | }, | ||
| 737 | "binding": { | ||
| 738 | "defaultHeaders": null | ||
| 739 | } | ||
| 740 | } | ||
| 741 | } | ||
| 742 | } | ||
| 743 | `, | ||
| 744 | ); | ||
| 745 | |||
| 746 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 747 | workspaceRoot, | ||
| 748 | homeConfigPath, | ||
| 749 | homeStateDirectory, | ||
| 750 | }); | ||
| 751 | const installation = deriveLocalInstallation(context); | ||
| 752 | const source = yield* createSourceFromPayload({ | ||
| 753 | workspaceId: installation.workspaceId, | ||
| 754 | sourceId, | ||
| 755 | payload: { | ||
| 756 | name: "GraphQL API", | ||
| 757 | kind: "graphql", | ||
| 758 | endpoint: "https://example.com/graphql", | ||
| 759 | status: "connected", | ||
| 760 | enabled: true, | ||
| 761 | namespace: "graphql", | ||
| 762 | importAuthPolicy: "reuse_runtime", | ||
| 763 | binding: { | ||
| 764 | defaultHeaders: null, | ||
| 765 | }, | ||
| 766 | importAuth: { kind: "none" }, | ||
| 767 | auth: { kind: "none" }, | ||
| 768 | }, | ||
| 769 | now, | ||
| 770 | }).pipe(Effect.orDie); | ||
| 771 | |||
| 772 | const artifact = buildLocalSourceArtifact({ | ||
| 773 | source, | ||
| 774 | syncResult: { | ||
| 775 | fragment: createGraphqlCatalogFragment({ | ||
| 776 | source, | ||
| 777 | documents: [ | ||
| 778 | { | ||
| 779 | documentKind: "graphql_introspection", | ||
| 780 | documentKey: source.endpoint, | ||
| 781 | contentText: '{"__schema":{}}', | ||
| 782 | fetchedAt: now, | ||
| 783 | }, | ||
| 784 | ], | ||
| 785 | operations: [ | ||
| 786 | { | ||
| 787 | toolId: "viewer", | ||
| 788 | title: "Viewer", | ||
| 789 | description: "Load the current viewer", | ||
| 790 | effect: "read", | ||
| 791 | inputSchema: { type: "object", properties: {} }, | ||
| 792 | outputSchema: { | ||
| 793 | type: "object", | ||
| 794 | properties: { login: { type: "string" } }, | ||
| 795 | }, | ||
| 796 | providerData: { | ||
| 797 | kind: "graphql", | ||
| 798 | toolKind: "field", | ||
| 799 | toolId: "viewer", | ||
| 800 | rawToolId: "viewer", | ||
| 801 | group: "query", | ||
| 802 | leaf: "viewer", | ||
| 803 | fieldName: "viewer", | ||
| 804 | operationType: "query", | ||
| 805 | operationName: "ViewerQuery", | ||
| 806 | operationDocument: "query ViewerQuery { viewer { login } }", | ||
| 807 | queryTypeName: "Query", | ||
| 808 | mutationTypeName: null, | ||
| 809 | subscriptionTypeName: null, | ||
| 810 | }, | ||
| 811 | }, | ||
| 812 | ], | ||
| 813 | }), | ||
| 814 | importMetadata: createCatalogImportMetadata({ | ||
| 815 | source, | ||
| 816 | adapterKey: "graphql", | ||
| 817 | }), | ||
| 818 | sourceHash: source.sourceHash, | ||
| 819 | }, | ||
| 820 | }); | ||
| 821 | |||
| 822 | yield* writeLocalWorkspaceState({ | ||
| 823 | context, | ||
| 824 | state: { | ||
| 825 | version: 1, | ||
| 826 | sources: { | ||
| 827 | [sourceId]: { | ||
| 828 | status: source.status, | ||
| 829 | lastError: source.lastError, | ||
| 830 | sourceHash: source.sourceHash, | ||
| 831 | createdAt: source.createdAt, | ||
| 832 | updatedAt: source.updatedAt, | ||
| 833 | }, | ||
| 834 | }, | ||
| 835 | policies: {}, | ||
| 836 | }, | ||
| 837 | }); | ||
| 838 | yield* writeLocalControlPlaneState({ | ||
| 839 | context, | ||
| 840 | state: { | ||
| 841 | version: 1, | ||
| 842 | authArtifacts: [], | ||
| 843 | authLeases: [], | ||
| 844 | sourceOauthClients: [], | ||
| 845 | workspaceOauthClients: [], | ||
| 846 | providerAuthGrants: [], | ||
| 847 | sourceAuthSessions: [], | ||
| 848 | secretMaterials: [], | ||
| 849 | executions: [], | ||
| 850 | executionInteractions: [], | ||
| 851 | executionSteps: [], | ||
| 852 | }, | ||
| 853 | }); | ||
| 854 | yield* writeLocalSourceArtifact({ | ||
| 855 | context, | ||
| 856 | sourceId, | ||
| 857 | artifact, | ||
| 858 | }); | ||
| 859 | |||
| 860 | const artifactPath = join(context.artifactsDirectory, "sources", `${sourceId}.json`); | ||
| 861 | const legacyArtifact = JSON.parse( | ||
| 862 | yield* fs.readFileString(artifactPath, "utf8"), | ||
| 863 | ) as Record<string, unknown>; | ||
| 864 | legacyArtifact.version = 3; | ||
| 865 | yield* fs.writeFileString(artifactPath, `${JSON.stringify(legacyArtifact)}\n`); | ||
| 866 | |||
| 867 | const runtime = yield* Effect.acquireRelease( | ||
| 868 | createControlPlaneRuntime({ | ||
| 869 | workspaceRoot, | ||
| 870 | homeConfigPath, | ||
| 871 | homeStateDirectory, | ||
| 872 | }), | ||
| 873 | (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 874 | ); | ||
| 875 | |||
| 876 | const inspection = yield* withControlPlaneClient( | ||
| 877 | { runtime, accountId: installation.accountId }, | ||
| 878 | (client) => | ||
| 879 | client.sources.inspection({ | ||
| 880 | path: { | ||
| 881 | workspaceId: installation.workspaceId, | ||
| 882 | sourceId, | ||
| 883 | }, | ||
| 884 | }), | ||
| 885 | ); | ||
| 886 | |||
| 887 | expect(inspection.source.id).toBe(sourceId); | ||
| 888 | expect(inspection.source.kind).toBe("graphql"); | ||
| 889 | expect(inspection.toolCount).toBe(1); | ||
| 890 | expect(inspection.tools.map((tool) => tool.path)).toContain("graphql.viewer"); | ||
| 891 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 892 | ); | ||
| 893 | |||
| 702 | it.scoped("returns an empty inspection bundle for auth-required sources without a catalog artifact", () => | 894 | it.scoped("returns an empty inspection bundle for auth-required sources without a catalog artifact", () => |
| 703 | Effect.gen(function* () { | 895 | Effect.gen(function* () { |
| 704 | const runtime = yield* makeRuntime; | 896 | const runtime = yield* makeRuntime; |
| @@ -932,6 +1124,175 @@ describe("control-plane-runtime", () => { · 227 unmodified lines | |||
| 932 | }), | 1124 | }), |
| 933 | ); | 1125 | ); |
| 934 | 1126 | ||
| 1127 | it.scoped("reuses existing Google provider grants for single connects and clears orphan state", () => | ||
| 1128 | Effect.gen(function* () { | ||
| 1129 | const runtime = yield* makeRuntime; | ||
| 1130 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1131 | const installation = runtime.localInstallation; | ||
| 1132 | |||
| 1133 | const oauthClient = yield* withControlPlaneClient( | ||
| 1134 | { runtime }, | ||
| 1135 | (client) => | ||
| 1136 | client.sources.createWorkspaceOauthClient({ | ||
| 1137 | path: { | ||
| 1138 | workspaceId: installation.workspaceId, | ||
| 1139 | }, | ||
| 1140 | payload: { | ||
| 1141 | providerKey: "google_workspace", | ||
| 1142 | label: "Local Google Workspace Client", | ||
| 1143 | oauthClient: { | ||
| 1144 | clientId: "google-client-id", | ||
| 1145 | clientSecret: "google-client-secret", | ||
| 1146 | }, | ||
| 1147 | }, | ||
| 1148 | }), | ||
| 1149 | ); | ||
| 1150 | |||
| 1151 | const refreshSecretId = "sec_google_refresh_single_reuse"; | ||
| 1152 | yield* upsertLocalSecret(runtime, { | ||
| 1153 | id: refreshSecretId, | ||
| 1154 | name: "Google Refresh Token", | ||
| 1155 | purpose: "oauth_refresh_token", | ||
| 1156 | value: "refresh-token-reuse", | ||
| 1157 | }); | ||
| 1158 | |||
| 1159 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1160 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1161 | id: grantId, | ||
| 1162 | workspaceId: installation.workspaceId, | ||
| 1163 | actorAccountId: installation.accountId, | ||
| 1164 | providerKey: "google_workspace", | ||
| 1165 | oauthClientId: oauthClient.id, | ||
| 1166 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1167 | clientAuthentication: "client_secret_post", | ||
| 1168 | headerName: "Authorization", | ||
| 1169 | prefix: "Bearer ", | ||
| 1170 | refreshToken: { | ||
| 1171 | providerId: "local", | ||
| 1172 | handle: refreshSecretId, | ||
| 1173 | }, | ||
| 1174 | grantedScopes: [ | ||
| 1175 | "scope:drive.readonly", | ||
| 1176 | ], | ||
| 1177 | lastRefreshedAt: null, | ||
| 1178 | orphanedAt: 123, | ||
| 1179 | createdAt: Date.now(), | ||
| 1180 | updatedAt: Date.now(), | ||
| 1181 | }); | ||
| 1182 | |||
| 1183 | googleServer.queueTokenResponse({ | ||
| 1184 | access_token: "reuse-access-token", | ||
| 1185 | scope: "scope:drive.readonly", | ||
| 1186 | }); | ||
| 1187 | |||
| 1188 | const result = yield* withControlPlaneClient( | ||
| 1189 | { runtime }, | ||
| 1190 | (client) => | ||
| 1191 | client.sources.connect({ | ||
| 1192 | path: { | ||
| 1193 | workspaceId: installation.workspaceId, | ||
| 1194 | }, | ||
| 1195 | payload: { | ||
| 1196 | kind: "google_discovery", | ||
| 1197 | workspaceOauthClientId: oauthClient.id, | ||
| 1198 | service: "drive", | ||
| 1199 | version: "v3", | ||
| 1200 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1201 | service: "drive", | ||
| 1202 | version: "v3", | ||
| 1203 | scope: "scope:drive.readonly", | ||
| 1204 | }), | ||
| 1205 | scopes: ["scope:drive.readonly"], | ||
| 1206 | name: "Drive", | ||
| 1207 | namespace: "google.drive", | ||
| 1208 | }, | ||
| 1209 | }), | ||
| 1210 | ); | ||
| 1211 | |||
| 1212 | expect(result.kind).toBe("connected"); | ||
| 1213 | if (result.kind !== "connected") { | ||
| 1214 | throw new Error(`Expected connected result, received ${result.kind}`); | ||
| 1215 | } | ||
| 1216 | expect(result.source.auth).toEqual({ | ||
| 1217 | kind: "provider_grant_ref", | ||
| 1218 | grantId, | ||
| 1219 | providerKey: "google_workspace", | ||
| 1220 | requiredScopes: ["scope:drive.readonly"], | ||
| 1221 | headerName: "Authorization", | ||
| 1222 | prefix: "Bearer ", | ||
| 1223 | }); | ||
| 1224 | |||
| 1225 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1226 | assertTrue(Option.isSome(storedGrant)); | ||
| 1227 | expect(storedGrant.value.orphanedAt).toBeNull(); | ||
| 1228 | expect(googleServer.discoveryAuthorizations).toContain("Bearer reuse-access-token"); | ||
| 1229 | }), | ||
| 1230 | 60_000, | ||
| 1231 | ); | ||
| 1232 | |||
| 1233 | it.scoped("uses the app callback redirect for browser-started Google single-source OAuth", () => | ||
| 1234 | Effect.gen(function* () { | ||
| 1235 | const runtime = yield* makeRuntime; | ||
| 1236 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1237 | const installation = runtime.localInstallation; | ||
| 1238 | |||
| 1239 | const oauthClient = yield* withControlPlaneClient( | ||
| 1240 | { runtime }, | ||
| 1241 | (client) => | ||
| 1242 | client.sources.createWorkspaceOauthClient({ | ||
| 1243 | path: { | ||
| 1244 | workspaceId: installation.workspaceId, | ||
| 1245 | }, | ||
| 1246 | payload: { | ||
| 1247 | providerKey: "google_workspace", | ||
| 1248 | label: "Local Google Workspace Client", | ||
| 1249 | oauthClient: { | ||
| 1250 | clientId: "google-client-id", | ||
| 1251 | clientSecret: "google-client-secret", | ||
| 1252 | }, | ||
| 1253 | }, | ||
| 1254 | }), | ||
| 1255 | ); | ||
| 1256 | |||
| 1257 | const result = yield* withControlPlaneClient( | ||
| 1258 | { runtime }, | ||
| 1259 | (client) => | ||
| 1260 | client.sources.connect({ | ||
| 1261 | path: { | ||
| 1262 | workspaceId: installation.workspaceId, | ||
| 1263 | }, | ||
| 1264 | payload: { | ||
| 1265 | kind: "google_discovery", | ||
| 1266 | workspaceOauthClientId: oauthClient.id, | ||
| 1267 | service: "gmail", | ||
| 1268 | version: "v1", | ||
| 1269 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1270 | service: "gmail", | ||
| 1271 | version: "v1", | ||
| 1272 | scope: "scope:gmail.readonly", | ||
| 1273 | }), | ||
| 1274 | scopes: ["scope:gmail.readonly"], | ||
| 1275 | name: "Gmail", | ||
| 1276 | namespace: "google.gmail", | ||
| 1277 | }, | ||
| 1278 | }), | ||
| 1279 | ); | ||
| 1280 | |||
| 1281 | expect(result.kind).toBe("oauth_required"); | ||
| 1282 | if (result.kind !== "oauth_required") { | ||
| 1283 | throw new Error(`Expected oauth_required result, received ${result.kind}`); | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | const redirectUri = new URL(result.authorizationUrl).searchParams.get("redirect_uri"); | ||
| 1287 | expect(redirectUri).not.toBeNull(); | ||
| 1288 | expect(new URL(redirectUri!).pathname).toBe( | ||
| 1289 | `/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/oauth/provider/callback`, | ||
| 1290 | ); | ||
| 1291 | expect(result.source.status).toBe("auth_required"); | ||
| 1292 | }), | ||
| 1293 | 60_000, | ||
| 1294 | ); | ||
| 1295 | |||
| 935 | it.scoped("reuses existing Google provider grants for batch connects and clears orphan state", () => | 1296 | it.scoped("reuses existing Google provider grants for batch connects and clears orphan state", () => |
| 936 | Effect.gen(function* () { | 1297 | Effect.gen(function* () { |
| 937 | const runtime = yield* makeRuntime; | 1298 | const runtime = yield* makeRuntime; |
| @@ -23,7 +23,7 @@ import { · 22 unmodified lines | |||
| 23 | contentHash, | 23 | contentHash, |
| 24 | snapshotFromSourceCatalogSyncResult, | 24 | snapshotFromSourceCatalogSyncResult, |
| 25 | type SourceCatalogSyncResult, | 25 | type SourceCatalogSyncResult, |
| 26 | } from "../sources/catalog-sync-result"; | 26 | } from "@executor/source-core"; |
| 27 | import type { ResolvedLocalWorkspaceContext } from "./config"; | 27 | import type { ResolvedLocalWorkspaceContext } from "./config"; |
| 28 | import { | 28 | import { |
| 29 | LocalFileSystemError, | 29 | LocalFileSystemError, |
| @@ -34,8 +34,18 @@ import { · 4 unmodified lines | |||
| 34 | createSourceCatalogRevisionRecord, | 34 | createSourceCatalogRevisionRecord, |
| 35 | stableSourceCatalogId, | 35 | stableSourceCatalogId, |
| 36 | } from "../sources/source-definitions"; | 36 | } from "../sources/source-definitions"; |
| 37 | const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const; | ||
| 37 | const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const; | 38 | const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const; |
| 38 | 39 | ||
| 40 | const LegacyLocalSourceArtifactSchema = Schema.Struct({ | ||
| 41 | version: Schema.Literal(LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION), | ||
| 42 | sourceId: SourceIdSchema, | ||
| 43 | catalogId: SourceCatalogIdSchema, | ||
| 44 | generatedAt: TimestampMsSchema, | ||
| 45 | revision: StoredSourceCatalogRevisionRecordSchema, | ||
| 46 | snapshot: CatalogSnapshotV1Schema, | ||
| 47 | }); | ||
| 48 | |||
| 39 | export const LocalSourceArtifactSchema = Schema.Struct({ | 49 | export const LocalSourceArtifactSchema = Schema.Struct({ |
| 40 | version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION), | 50 | version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION), |
| 41 | sourceId: SourceIdSchema, | 51 | sourceId: SourceIdSchema, |
| @@ -46,8 +56,21 @@ export const LocalSourceArtifactSchema = Schema.Struct({ · 4 unmodified lines | |||
| 46 | }); | 56 | }); |
| 47 | 57 | ||
| 48 | export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; | 58 | export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; |
| 49 | 59 | type LegacyLocalSourceArtifact = typeof LegacyLocalSourceArtifactSchema.Type; | |
| 50 | const decodeLocalSourceArtifact = Schema.decodeUnknownSync(LocalSourceArtifactSchema); | 60 | |
| 61 | const decodeLocalSourceArtifact = Schema.decodeUnknownSync( | ||
| 62 | Schema.Union(LocalSourceArtifactSchema, LegacyLocalSourceArtifactSchema), | ||
| 63 | ); | ||
| 64 | |||
| 65 | const normalizeLocalSourceArtifact = ( | ||
| 66 | artifact: LocalSourceArtifact | LegacyLocalSourceArtifact, | ||
| 67 | ): LocalSourceArtifact => | ||
| 68 | artifact.version === LOCAL_SOURCE_ARTIFACT_VERSION | ||
| 69 | ? artifact | ||
| 70 | : { | ||
| 71 | ...artifact, | ||
| 72 | version: LOCAL_SOURCE_ARTIFACT_VERSION, | ||
| 73 | }; | ||
| 51 | 74 | ||
| 52 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | 75 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| 53 | new LocalFileSystemError({ | 76 | new LocalFileSystemError({ |
| @@ -212,7 +235,7 @@ export const readLocalSourceArtifact = (input: { · 158 unmodified lines | |||
| 212 | path: readPath, | 235 | path: readPath, |
| 213 | details: unknownLocalErrorDetails(cause), | 236 | details: unknownLocalErrorDetails(cause), |
| 214 | }), | 237 | }), |
| 215 | }); | 238 | }).pipe(Effect.map(normalizeLocalSourceArtifact)); |
| 216 | 239 | ||
| 217 | const rawDocuments: Record<string, NativeBlob> = {}; | 240 | const rawDocuments: Record<string, NativeBlob> = {}; |
| 218 | for (const documentId of Object.keys(artifact.snapshot.catalog.documents)) { | 241 | for (const documentId of Object.keys(artifact.snapshot.catalog.documents)) { |
| @@ -32,7 +32,7 @@ import { · 31 unmodified lines | |||
| 32 | loadLocalWorkspaceState, | 32 | loadLocalWorkspaceState, |
| 33 | writeLocalWorkspaceState, | 33 | writeLocalWorkspaceState, |
| 34 | } from "./workspace-state"; | 34 | } from "./workspace-state"; |
| 35 | import type { SourceCatalogSyncResult } from "../sources/catalog-sync-result"; | 35 | import type { SourceCatalogSyncResult } from "@executor/source-core"; |
| 36 | import type { Source } from "#schema"; | 36 | import type { Source } from "#schema"; |
| 37 | 37 | ||
| 38 | export type InstallationStoreShape = { | 38 | export type InstallationStoreShape = { |
| @@ -1,66 +1,48 @@ | |||
| 1 | import * as Schema from "effect/Schema"; | 1 | import * as Schema from "effect/Schema"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | createSourceAdapterRegistry, | 4 | createSourceAdapterComposition, |
| 5 | type SourceAdapter, | 5 | type SourceAdapter, |
| 6 | } from "@executor/source-core"; | 6 | } from "@executor/source-core"; |
| 7 | import { googleDiscoverySourceAdapter } from "@executor/source-google-discovery"; | 7 | import { externalSourceAdapters } from "@executor/source-builtins"; |
| 8 | import { graphqlSourceAdapter } from "@executor/source-graphql"; | ||
| 9 | import { mcpSourceAdapter } from "@executor/source-mcp"; | ||
| 10 | import { openApiSourceAdapter } from "@executor/source-openapi"; | ||
| 11 | 8 | ||
| 12 | import { internalSourceAdapter } from "./internal"; | 9 | import { internalSourceAdapter } from "./internal"; |
| 13 | 10 | ||
| 14 | export type * from "@executor/source-core"; | 11 | export type * from "@executor/source-core"; |
| 15 | 12 | ||
| 16 | export const builtInSourceAdapters = [ | 13 | export const builtInSourceAdapters = [ |
| 17 | openApiSourceAdapter, | 14 | ...externalSourceAdapters, |
| 18 | graphqlSourceAdapter, | ||
| 19 | googleDiscoverySourceAdapter, | ||
| 20 | mcpSourceAdapter, | ||
| 21 | internalSourceAdapter, | 15 | internalSourceAdapter, |
| 22 | ] as const satisfies readonly SourceAdapter[]; | 16 | ] as const satisfies readonly SourceAdapter[]; |
| 23 | 17 | const composition = createSourceAdapterComposition(builtInSourceAdapters); | |
| 24 | export const connectableSourceAdapters = [ | 18 | |
| 25 | mcpSourceAdapter, | 19 | export const connectableSourceAdapters = composition.connectableSourceAdapters; |
| 26 | openApiSourceAdapter, | 20 | export const ConnectSourcePayloadSchema = |
| 27 | graphqlSourceAdapter, | 21 | composition.connectPayloadSchema as Schema.Schema< |
| 28 | googleDiscoverySourceAdapter, | 22 | typeof composition.connectPayloadSchema.Type, |
| 29 | ] as const; | 23 | any, |
| 30 | 24 | never | |
| 31 | export const ConnectSourcePayloadSchema = Schema.Union( | 25 | >; |
| 32 | mcpSourceAdapter.connectPayloadSchema!, | ||
| 33 | openApiSourceAdapter.connectPayloadSchema!, | ||
| 34 | graphqlSourceAdapter.connectPayloadSchema!, | ||
| 35 | googleDiscoverySourceAdapter.connectPayloadSchema!, | ||
| 36 | ); | ||
| 37 | |||
| 38 | export type ConnectSourcePayload = typeof ConnectSourcePayloadSchema.Type; | 26 | export type ConnectSourcePayload = typeof ConnectSourcePayloadSchema.Type; |
| 39 | 27 | ||
| 40 | export const executorAddableSourceAdapters = [ | 28 | export const executorAddableSourceAdapters = composition.executorAddableSourceAdapters; |
| 41 | mcpSourceAdapter, | 29 | export const ExecutorAddSourceInputSchema = |
| 42 | openApiSourceAdapter, | 30 | composition.executorAddInputSchema as Schema.Schema< |
| 43 | graphqlSourceAdapter, | 31 | typeof composition.executorAddInputSchema.Type, |
| 44 | googleDiscoverySourceAdapter, | 32 | any, |
| 45 | ] as const; | 33 | never |
| 46 | 34 | >; | |
| 47 | export const ExecutorAddSourceInputSchema = Schema.Union( | ||
| 48 | mcpSourceAdapter.executorAddInputSchema!, | ||
| 49 | openApiSourceAdapter.executorAddInputSchema!, | ||
| 50 | graphqlSourceAdapter.executorAddInputSchema!, | ||
| 51 | googleDiscoverySourceAdapter.executorAddInputSchema!, | ||
| 52 | ); | ||
| 53 | |||
| 54 | export type ExecutorAddSourceInput = typeof ExecutorAddSourceInputSchema.Type; | 35 | export type ExecutorAddSourceInput = typeof ExecutorAddSourceInputSchema.Type; |
| 55 | 36 | ||
| 56 | const registry = createSourceAdapterRegistry(builtInSourceAdapters); | 37 | export const localConfigurableSourceAdapters = composition.localConfigurableSourceAdapters; |
| 57 | 38 | ||
| 58 | export const getSourceAdapter = registry.getSourceAdapter; | 39 | export const getSourceAdapter = composition.getSourceAdapter; |
| 59 | export const getSourceAdapterForSource = registry.getSourceAdapterForSource; | 40 | export const getSourceAdapterForSource = composition.getSourceAdapterForSource; |
| 60 | export const sourceBindingStateFromSource = registry.sourceBindingStateFromSource; | 41 | export const findSourceAdapterByProviderKey = composition.findSourceAdapterByProviderKey; |
| 61 | export const sourceAdapterCatalogKind = registry.sourceAdapterCatalogKind; | 42 | export const sourceBindingStateFromSource = composition.sourceBindingStateFromSource; |
| 43 | export const sourceAdapterCatalogKind = composition.sourceAdapterCatalogKind; | ||
| 62 | export const sourceAdapterRequiresInteractiveConnect = | 44 | export const sourceAdapterRequiresInteractiveConnect = |
| 63 | registry.sourceAdapterRequiresInteractiveConnect; | 45 | composition.sourceAdapterRequiresInteractiveConnect; |
| 64 | export const sourceAdapterUsesCredentialManagedAuth = | 46 | export const sourceAdapterUsesCredentialManagedAuth = |
| 65 | registry.sourceAdapterUsesCredentialManagedAuth; | 47 | composition.sourceAdapterUsesCredentialManagedAuth; |
| 66 | export const isInternalSourceAdapter = registry.isInternalSourceAdapter; | 48 | export const isInternalSourceAdapter = composition.isInternalSourceAdapter; |
| @@ -52,7 +52,7 @@ const internalBindingConfigFromSource = ( · 51 unmodified lines | |||
| 52 | }); | 52 | }); |
| 53 | }); | 53 | }); |
| 54 | 54 | ||
| 55 | export const internalSourceAdapter: SourceAdapter = { | 55 | export const internalSourceAdapter = { |
| 56 | key: "internal", | 56 | key: "internal", |
| 57 | displayName: "Internal", | 57 | displayName: "Internal", |
| 58 | catalogKind: "internal", | 58 | catalogKind: "internal", |
| @@ -65,6 +65,8 @@ export const internalSourceAdapter: SourceAdapter = { · 6 unmodified lines | |||
| 65 | executorAddInputSchema: null, | 65 | executorAddInputSchema: null, |
| 66 | executorAddHelpText: null, | 66 | executorAddHelpText: null, |
| 67 | executorAddInputSignatureWidth: null, | 67 | executorAddInputSignatureWidth: null, |
| 68 | localConfigBindingSchema: null, | ||
| 69 | localConfigBindingFromSource: () => null, | ||
| 68 | serializeBindingConfig: (source) => | 70 | serializeBindingConfig: (source) => |
| 69 | encodeBindingConfig({ | 71 | encodeBindingConfig({ |
| 70 | adapterKey: source.kind, | 72 | adapterKey: source.kind, |
| @@ -124,4 +126,4 @@ export const internalSourceAdapter: SourceAdapter = { · 53 unmodified lines | |||
| 124 | Effect.fail( | 126 | Effect.fail( |
| 125 | runtimeEffectError("sources/source-adapters/internal", "Internal sources do not support persisted adapter invocation"), | 127 | runtimeEffectError("sources/source-adapters/internal", "Internal sources do not support persisted adapter invocation"), |
| 126 | ), | 128 | ), |
| 127 | }; | 129 | } satisfies SourceAdapter; |
| @@ -4,6 +4,10 @@ import { · 3 unmodified lines | |||
| 4 | isMcpStdioTransport, | 4 | isMcpStdioTransport, |
| 5 | type McpDiscoveryElicitationContext, | 5 | type McpDiscoveryElicitationContext, |
| 6 | } from "@executor/source-mcp"; | 6 | } from "@executor/source-mcp"; |
| 7 | import { | ||
| 8 | isSourceCredentialRequiredError, | ||
| 9 | type SourceAdapterOauth2SetupConfig, | ||
| 10 | } from "@executor/source-core"; | ||
| 7 | import { | 11 | import { |
| 8 | AccountId, | 12 | AccountId, |
| 9 | type CredentialSlot, | 13 | type CredentialSlot, |
| @@ -61,14 +65,13 @@ import { · 51 unmodified lines | |||
| 61 | updateSourceFromPayload, | 65 | updateSourceFromPayload, |
| 62 | } from "./source-definitions"; | 66 | } from "./source-definitions"; |
| 63 | import { | 67 | import { |
| 68 | findSourceAdapterByProviderKey, | ||
| 64 | getSourceAdapter, | 69 | getSourceAdapter, |
| 65 | getSourceAdapterForSource, | 70 | getSourceAdapterForSource, |
| 66 | sourceAdapterRequiresInteractiveConnect, | 71 | sourceAdapterRequiresInteractiveConnect, |
| 67 | sourceAdapterUsesCredentialManagedAuth, | 72 | sourceAdapterUsesCredentialManagedAuth, |
| 68 | sourceBindingStateFromSource, | 73 | sourceBindingStateFromSource, |
| 69 | } from "./source-adapters"; | 74 | } from "./source-adapters"; |
| 70 | import type { SourceAdapterOauth2SetupConfig } from "./source-adapters/types"; | ||
| 71 | import { isSourceCredentialRequiredError } from "./source-adapters/shared"; | ||
| 72 | import { | 75 | import { |
| 73 | createDefaultSecretMaterialDeleter, | 76 | createDefaultSecretMaterialDeleter, |
| 74 | type ResolveSecretMaterial, | 77 | type ResolveSecretMaterial, |
| @@ -932,6 +935,24 @@ type ResolvedWorkspaceOauthClient = ResolvedSourceOauthClient & { · 857 unmodified lines | |||
| 932 | label: string | null; | 935 | label: string | null; |
| 933 | }; | 936 | }; |
| 934 | 937 | ||
| 938 | type RuntimeProviderAuthTarget = { | ||
| 939 | source: Source; | ||
| 940 | requiredScopes: ReadonlyArray<string>; | ||
| 941 | setupConfig: SourceAdapterOauth2SetupConfig; | ||
| 942 | }; | ||
| 943 | |||
| 944 | type RuntimeProviderAuthResult = | ||
| 945 | | { | ||
| 946 | kind: "connected"; | ||
| 947 | sources: ReadonlyArray<Source>; | ||
| 948 | } | ||
| 949 | | { | ||
| 950 | kind: "oauth_required"; | ||
| 951 | sources: ReadonlyArray<Source>; | ||
| 952 | sessionId: SourceAuthSession["id"]; | ||
| 953 | authorizationUrl: string; | ||
| 954 | }; | ||
| 955 | |||
| 935 | const decodeWorkspaceSourceOauthClientMetadataOption = Schema.decodeUnknownOption( | 956 | const decodeWorkspaceSourceOauthClientMetadataOption = Schema.decodeUnknownOption( |
| 936 | WorkspaceSourceOauthClientMetadataJsonSchema, | 957 | WorkspaceSourceOauthClientMetadataJsonSchema, |
| 937 | ); | 958 | ); |
| @@ -1172,6 +1193,84 @@ const providerGrantCoversScopes = ( · 234 unmodified lines | |||
| 1172 | const normalizeScopes = (scopes: ReadonlyArray<string>): ReadonlyArray<string> => | 1193 | const normalizeScopes = (scopes: ReadonlyArray<string>): ReadonlyArray<string> => |
| 1173 | [...new Set(scopes.map((scope) => scope.trim()).filter((scope) => scope.length > 0))]; | 1194 | [...new Set(scopes.map((scope) => scope.trim()).filter((scope) => scope.length > 0))]; |
| 1174 | 1195 | ||
| 1196 | const authorizationParamsMatch = ( | ||
| 1197 | left: SourceAdapterOauth2SetupConfig["authorizationParams"], | ||
| 1198 | right: SourceAdapterOauth2SetupConfig["authorizationParams"], | ||
| 1199 | ): boolean => { | ||
| 1200 | const leftEntries = Object.entries(left ?? {}).sort(([leftKey], [rightKey]) => | ||
| 1201 | leftKey.localeCompare(rightKey), | ||
| 1202 | ); | ||
| 1203 | const rightEntries = Object.entries(right ?? {}).sort(([leftKey], [rightKey]) => | ||
| 1204 | leftKey.localeCompare(rightKey), | ||
| 1205 | ); | ||
| 1206 | |||
| 1207 | return leftEntries.length === rightEntries.length | ||
| 1208 | && leftEntries.every(([key, value], index) => { | ||
| 1209 | const otherEntry = rightEntries[index]; | ||
| 1210 | return otherEntry !== undefined | ||
| 1211 | && otherEntry[0] === key | ||
| 1212 | && otherEntry[1] === value; | ||
| 1213 | }); | ||
| 1214 | }; | ||
| 1215 | |||
| 1216 | const providerOauthSetupConfigMatches = ( | ||
| 1217 | left: SourceAdapterOauth2SetupConfig, | ||
| 1218 | right: SourceAdapterOauth2SetupConfig, | ||
| 1219 | ): boolean => | ||
| 1220 | left.providerKey === right.providerKey | ||
| 1221 | && left.authorizationEndpoint === right.authorizationEndpoint | ||
| 1222 | && left.tokenEndpoint === right.tokenEndpoint | ||
| 1223 | && left.headerName === right.headerName | ||
| 1224 | && left.prefix === right.prefix | ||
| 1225 | && left.clientAuthentication === right.clientAuthentication | ||
| 1226 | && authorizationParamsMatch(left.authorizationParams, right.authorizationParams); | ||
| 1227 | |||
| 1228 | const resolveRuntimeProviderAuthTarget = ( | ||
| 1229 | source: Source, | ||
| 1230 | ): Effect.Effect<RuntimeProviderAuthTarget | null, Error, never> => | ||
| 1231 | Effect.gen(function* () { | ||
| 1232 | const adapter = getSourceAdapterForSource(source); | ||
| 1233 | const setupConfig = adapter.getOauth2SetupConfig | ||
| 1234 | ? yield* adapter.getOauth2SetupConfig({ | ||
| 1235 | source, | ||
| 1236 | slot: "runtime", | ||
| 1237 | }) | ||
| 1238 | : null; | ||
| 1239 | if (setupConfig === null) { | ||
| 1240 | return null; | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | return { | ||
| 1244 | source, | ||
| 1245 | requiredScopes: normalizeScopes(setupConfig.scopes), | ||
| 1246 | setupConfig, | ||
| 1247 | } satisfies RuntimeProviderAuthTarget; | ||
| 1248 | }); | ||
| 1249 | |||
| 1250 | const mergeRuntimeProviderAuthSetupConfig = ( | ||
| 1251 | targets: ReadonlyArray<RuntimeProviderAuthTarget>, | ||
| 1252 | ): Effect.Effect<SourceAdapterOauth2SetupConfig, Error, never> => | ||
| 1253 | Effect.gen(function* () { | ||
| 1254 | if (targets.length === 0) { | ||
| 1255 | return yield* runtimeEffectError("sources/source-auth-service", "Provider auth setup requires at least one target source"); | ||
| 1256 | } | ||
| 1257 | |||
| 1258 | const baseSetupConfig = targets[0]!.setupConfig; | ||
| 1259 | for (const target of targets.slice(1)) { | ||
| 1260 | if (!providerOauthSetupConfigMatches(baseSetupConfig, target.setupConfig)) { | ||
| 1261 | return yield* runtimeEffectError( | ||
| 1262 | "sources/source-auth-service", | ||
| 1263 | `Provider auth setup for ${baseSetupConfig.providerKey} requires compatible source OAuth configuration`, | ||
| 1264 | ); | ||
| 1265 | } | ||
| 1266 | } | ||
| 1267 | |||
| 1268 | return { | ||
| 1269 | ...baseSetupConfig, | ||
| 1270 | scopes: normalizeScopes(targets.flatMap((target) => [...target.requiredScopes])), | ||
| 1271 | }; | ||
| 1272 | }); | ||
| 1273 | |||
| 1175 | const findReusableProviderGrant = (input: { | 1274 | const findReusableProviderGrant = (input: { |
| 1176 | rows: ControlPlaneStoreShape; | 1275 | rows: ControlPlaneStoreShape; |
| 1177 | workspaceId: WorkspaceId; | 1276 | workspaceId: WorkspaceId; |
| @@ -1512,6 +1611,96 @@ const startProviderOauthBatchCredentialSetup = (input: { · 334 unmodified lines | |||
| 1512 | ); | 1611 | ); |
| 1513 | }); | 1612 | }); |
| 1514 | 1613 | ||
| 1614 | const connectSourcesWithProviderRuntimeAuth = (input: { | ||
| 1615 | rows: ControlPlaneStoreShape; | ||
| 1616 | sourceStore: RuntimeSourceStore; | ||
| 1617 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; | ||
| 1618 | workspaceId: WorkspaceId; | ||
| 1619 | actorAccountId?: AccountId | null; | ||
| 1620 | executionId?: SourceAuthSession["executionId"]; | ||
| 1621 | interactionId?: SourceAuthSession["interactionId"]; | ||
| 1622 | baseUrl?: string | null; | ||
| 1623 | getLocalServerBaseUrl?: () => string | undefined; | ||
| 1624 | workspaceOauthClient: ResolvedWorkspaceOauthClient; | ||
| 1625 | targets: ReadonlyArray<RuntimeProviderAuthTarget>; | ||
| 1626 | }): Effect.Effect<RuntimeProviderAuthResult, Error, WorkspaceStorageServices> => | ||
| 1627 | Effect.gen(function* () { | ||
| 1628 | const setupConfig = yield* mergeRuntimeProviderAuthSetupConfig(input.targets); | ||
| 1629 | const reusableGrant = yield* findReusableProviderGrant({ | ||
| 1630 | rows: input.rows, | ||
| 1631 | workspaceId: input.workspaceId, | ||
| 1632 | actorAccountId: input.actorAccountId, | ||
| 1633 | providerKey: setupConfig.providerKey, | ||
| 1634 | oauthClientId: input.workspaceOauthClient.id, | ||
| 1635 | requiredScopes: setupConfig.scopes, | ||
| 1636 | }); | ||
| 1637 | |||
| 1638 | if (Option.isSome(reusableGrant)) { | ||
| 1639 | const connectedSources = yield* attachProviderGrantToSources({ | ||
| 1640 | sourceStore: input.sourceStore, | ||
| 1641 | sourceCatalogSync: input.sourceCatalogSync, | ||
| 1642 | actorAccountId: input.actorAccountId, | ||
| 1643 | grantId: reusableGrant.value.id, | ||
| 1644 | providerKey: reusableGrant.value.providerKey, | ||
| 1645 | headerName: reusableGrant.value.headerName, | ||
| 1646 | prefix: reusableGrant.value.prefix, | ||
| 1647 | targets: input.targets.map((target) => ({ | ||
| 1648 | source: target.source, | ||
| 1649 | requiredScopes: target.requiredScopes, | ||
| 1650 | })), | ||
| 1651 | }); | ||
| 1652 | |||
| 1653 | return { | ||
| 1654 | kind: "connected", | ||
| 1655 | sources: connectedSources, | ||
| 1656 | } satisfies RuntimeProviderAuthResult; | ||
| 1657 | } | ||
| 1658 | |||
| 1659 | const requestBaseUrl = trimOrNull(input.baseUrl); | ||
| 1660 | const baseUrl = requestBaseUrl ?? input.getLocalServerBaseUrl?.() ?? null; | ||
| 1661 | if (baseUrl === null) { | ||
| 1662 | return yield* runtimeEffectError( | ||
| 1663 | "sources/source-auth-service", | ||
| 1664 | `Local executor server base URL is unavailable for ${setupConfig.providerKey} OAuth setup`, | ||
| 1665 | ); | ||
| 1666 | } | ||
| 1667 | |||
| 1668 | const oauthRequired = yield* startProviderOauthBatchCredentialSetup({ | ||
| 1669 | rows: input.rows, | ||
| 1670 | sourceStore: input.sourceStore, | ||
| 1671 | workspaceId: input.workspaceId, | ||
| 1672 | actorAccountId: input.actorAccountId, | ||
| 1673 | executionId: input.executionId, | ||
| 1674 | interactionId: input.interactionId, | ||
| 1675 | baseUrl, | ||
| 1676 | redirectModeOverride: requestBaseUrl ? "app_callback" : undefined, | ||
| 1677 | workspaceOauthClient: input.workspaceOauthClient, | ||
| 1678 | setupConfig, | ||
| 1679 | targetSources: input.targets.map((target) => ({ | ||
| 1680 | source: target.source, | ||
| 1681 | requiredScopes: target.requiredScopes, | ||
| 1682 | })), | ||
| 1683 | }); | ||
| 1684 | |||
| 1685 | const pendingSources = yield* Effect.forEach( | ||
| 1686 | input.targets, | ||
| 1687 | (target) => | ||
| 1688 | input.sourceStore.loadSourceById({ | ||
| 1689 | workspaceId: target.source.workspaceId, | ||
| 1690 | sourceId: target.source.id, | ||
| 1691 | actorAccountId: input.actorAccountId, | ||
| 1692 | }), | ||
| 1693 | { discard: false }, | ||
| 1694 | ); | ||
| 1695 | |||
| 1696 | return { | ||
| 1697 | kind: "oauth_required", | ||
| 1698 | sources: pendingSources, | ||
| 1699 | sessionId: oauthRequired.sessionId, | ||
| 1700 | authorizationUrl: oauthRequired.authorizationUrl, | ||
| 1701 | } satisfies RuntimeProviderAuthResult; | ||
| 1702 | }); | ||
| 1703 | |||
| 1515 | const attachProviderGrantToSources = (input: { | 1704 | const attachProviderGrantToSources = (input: { |
| 1516 | sourceStore: RuntimeSourceStore; | 1705 | sourceStore: RuntimeSourceStore; |
| 1517 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; | 1706 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; |
| @@ -2235,15 +2424,10 @@ const addExecutorGoogleDiscoverySource = (input: { · 717 unmodified lines | |||
| 2235 | actorAccountId: input.sourceInput.actorAccountId, | 2424 | actorAccountId: input.sourceInput.actorAccountId, |
| 2236 | }); | 2425 | }); |
| 2237 | const googleAdapter = getSourceAdapterForSource(persistedDraft); | 2426 | const googleAdapter = getSourceAdapterForSource(persistedDraft); |
| 2238 | const googleSetupConfig = googleAdapter.getOauth2SetupConfig | 2427 | const providerAuthTarget = yield* resolveRuntimeProviderAuthTarget(persistedDraft); |
| 2239 | ? yield* googleAdapter.getOauth2SetupConfig({ | ||
| 2240 | source: persistedDraft, | ||
| 2241 | slot: "runtime", | ||
| 2242 | }) | ||
| 2243 | : null; | ||
| 2244 | 2428 | ||
| 2245 | if ( | 2429 | if ( |
| 2246 | googleSetupConfig !== null | 2430 | providerAuthTarget !== null |
| 2247 | && input.sourceInput.auth === undefined | 2431 | && input.sourceInput.auth === undefined |
| 2248 | && persistedDraft.auth.kind === "none" | 2432 | && persistedDraft.auth.kind === "none" |
| 2249 | ) { | 2433 | ) { |
| @@ -2253,13 +2437,13 @@ const addExecutorGoogleDiscoverySource = (input: { · 3 unmodified lines | |||
| 2253 | rows: input.rows, | 2437 | rows: input.rows, |
| 2254 | workspaceId: persistedDraft.workspaceId, | 2438 | workspaceId: persistedDraft.workspaceId, |
| 2255 | oauthClientId: input.sourceInput.workspaceOauthClientId, | 2439 | oauthClientId: input.sourceInput.workspaceOauthClientId, |
| 2256 | providerKey: googleSetupConfig.providerKey, | 2440 | providerKey: providerAuthTarget.setupConfig.providerKey, |
| 2257 | }); | 2441 | }); |
| 2258 | } else if (input.sourceInput.oauthClient) { | 2442 | } else if (input.sourceInput.oauthClient) { |
| 2259 | workspaceOauthClient = yield* createWorkspaceOauthClient({ | 2443 | workspaceOauthClient = yield* createWorkspaceOauthClient({ |
| 2260 | rows: input.rows, | 2444 | rows: input.rows, |
| 2261 | workspaceId: persistedDraft.workspaceId, | 2445 | workspaceId: persistedDraft.workspaceId, |
| 2262 | providerKey: googleSetupConfig.providerKey, | 2446 | providerKey: providerAuthTarget.setupConfig.providerKey, |
| 2263 | oauthClient: input.sourceInput.oauthClient, | 2447 | oauthClient: input.sourceInput.oauthClient, |
| 2264 | label: `${chosenName} OAuth Client`, | 2448 | label: `${chosenName} OAuth Client`, |
| 2265 | normalizeOauthClient: googleAdapter.normalizeOauthClientInput, | 2449 | normalizeOauthClient: googleAdapter.normalizeOauthClientInput, |
| @@ -2268,69 +2452,37 @@ const addExecutorGoogleDiscoverySource = (input: { · 2 unmodified lines | |||
| 2268 | } | 2452 | } |
| 2269 | 2453 | ||
| 2270 | if (workspaceOauthClient === null) { | 2454 | if (workspaceOauthClient === null) { |
| 2271 | return yield* runtimeEffectError("sources/source-auth-service", "Google shared auth requires a workspace OAuth client"); | 2455 | return yield* runtimeEffectError( |
| 2272 | } | 2456 | "sources/source-auth-service", |
| 2273 | 2457 | `${providerAuthTarget.setupConfig.providerKey} shared auth requires a workspace OAuth client`, | |
| 2274 | const requiredScopes = normalizeScopes(googleSetupConfig.scopes); | 2458 | ); |
| 2275 | const reusableGrant = yield* findReusableProviderGrant({ | ||
| 2276 | rows: input.rows, | ||
| 2277 | workspaceId: persistedDraft.workspaceId, | ||
| 2278 | actorAccountId: input.sourceInput.actorAccountId, | ||
| 2279 | providerKey: googleSetupConfig.providerKey, | ||
| 2280 | oauthClientId: workspaceOauthClient.id, | ||
| 2281 | requiredScopes, | ||
| 2282 | }); | ||
| 2283 | |||
| 2284 | if (Option.isSome(reusableGrant)) { | ||
| 2285 | const [connectedSource] = yield* attachProviderGrantToSources({ | ||
| 2286 | sourceStore: input.sourceStore, | ||
| 2287 | sourceCatalogSync: input.sourceCatalogSync, | ||
| 2288 | actorAccountId: input.sourceInput.actorAccountId, | ||
| 2289 | grantId: reusableGrant.value.id, | ||
| 2290 | providerKey: reusableGrant.value.providerKey, | ||
| 2291 | headerName: reusableGrant.value.headerName, | ||
| 2292 | prefix: reusableGrant.value.prefix, | ||
| 2293 | targets: [{ | ||
| 2294 | source: persistedDraft, | ||
| 2295 | requiredScopes, | ||
| 2296 | }], | ||
| 2297 | }); | ||
| 2298 | |||
| 2299 | return { | ||
| 2300 | kind: "connected", | ||
| 2301 | source: connectedSource!, | ||
| 2302 | } satisfies ExecutorSourceAddResult; | ||
| 2303 | } | ||
| 2304 | |||
| 2305 | const requestBaseUrl = trimOrNull(input.baseUrl); | ||
| 2306 | const baseUrl = requestBaseUrl ?? input.getLocalServerBaseUrl?.() ?? null; | ||
| 2307 | if (baseUrl === null) { | ||
| 2308 | return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup"); | ||
| 2309 | } | 2459 | } |
| 2310 | 2460 | ||
| 2311 | const oauthRequired = yield* startProviderOauthBatchCredentialSetup({ | 2461 | const providerAuthResult = yield* connectSourcesWithProviderRuntimeAuth({ |
| 2312 | rows: input.rows, | 2462 | rows: input.rows, |
| 2313 | sourceStore: input.sourceStore, | 2463 | sourceStore: input.sourceStore, |
| 2464 | sourceCatalogSync: input.sourceCatalogSync, | ||
| 2314 | workspaceId: persistedDraft.workspaceId, | 2465 | workspaceId: persistedDraft.workspaceId, |
| 2315 | actorAccountId: input.sourceInput.actorAccountId, | 2466 | actorAccountId: input.sourceInput.actorAccountId, |
| 2316 | executionId: input.sourceInput.executionId, | 2467 | executionId: input.sourceInput.executionId, |
| 2317 | interactionId: input.sourceInput.interactionId, | 2468 | interactionId: input.sourceInput.interactionId, |
| 2318 | baseUrl, | 2469 | baseUrl: input.baseUrl, |
| 2319 | redirectModeOverride: requestBaseUrl ? "app_callback" : undefined, | 2470 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| 2320 | workspaceOauthClient, | 2471 | workspaceOauthClient, |
| 2321 | setupConfig: googleSetupConfig, | 2472 | targets: [providerAuthTarget], |
| 2322 | targetSources: [{ | ||
| 2323 | source: persistedDraft, | ||
| 2324 | requiredScopes, | ||
| 2325 | }], | ||
| 2326 | }); | 2473 | }); |
| 2327 | 2474 | ||
| 2328 | return { | 2475 | return providerAuthResult.kind === "connected" |
| 2329 | kind: "oauth_required", | 2476 | ? { |
| 2330 | source: oauthRequired.source, | 2477 | kind: "connected", |
| 2331 | sessionId: oauthRequired.sessionId, | 2478 | source: providerAuthResult.sources[0]!, |
| 2332 | authorizationUrl: oauthRequired.authorizationUrl, | 2479 | } satisfies ExecutorSourceAddResult |
| 2333 | } satisfies ExecutorSourceAddResult; | 2480 | : { |
| 2481 | kind: "oauth_required", | ||
| 2482 | source: providerAuthResult.sources[0]!, | ||
| 2483 | sessionId: providerAuthResult.sessionId, | ||
| 2484 | authorizationUrl: providerAuthResult.authorizationUrl, | ||
| 2485 | } satisfies ExecutorSourceAddResult; | ||
| 2334 | } | 2486 | } |
| 2335 | 2487 | ||
| 2336 | if (input.sourceInput.oauthClient) { | 2488 | if (input.sourceInput.oauthClient) { |
| @@ -2537,66 +2689,43 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 200 unmodified lines | |||
| 2537 | const persistedDraft = yield* input.sourceStore.persistSource(draftSource, { | 2689 | const persistedDraft = yield* input.sourceStore.persistSource(draftSource, { |
| 2538 | actorAccountId: input.sourceInput.actorAccountId, | 2690 | actorAccountId: input.sourceInput.actorAccountId, |
| 2539 | }); | 2691 | }); |
| 2540 | const adapter = getSourceAdapterForSource(persistedDraft); | 2692 | const providerAuthTarget = yield* resolveRuntimeProviderAuthTarget(persistedDraft); |
| 2541 | const setupConfig = adapter.getOauth2SetupConfig | 2693 | if (providerAuthTarget === null) { |
| 2542 | ? yield* adapter.getOauth2SetupConfig({ | ||
| 2543 | source: persistedDraft, | ||
| 2544 | slot: "runtime", | ||
| 2545 | }) | ||
| 2546 | : null; | ||
| 2547 | if (setupConfig === null) { | ||
| 2548 | return yield* runtimeEffectError("sources/source-auth-service", `Source ${persistedDraft.id} does not support Google shared auth`); | 2694 | return yield* runtimeEffectError("sources/source-auth-service", `Source ${persistedDraft.id} does not support Google shared auth`); |
| 2549 | } | 2695 | } |
| 2550 | 2696 | ||
| 2551 | return { | 2697 | return providerAuthTarget; |
| 2552 | source: persistedDraft, | ||
| 2553 | requiredScopes: normalizeScopes(setupConfig.scopes), | ||
| 2554 | setupConfig, | ||
| 2555 | }; | ||
| 2556 | }), | 2698 | }), |
| 2557 | { discard: false }, | 2699 | { discard: false }, |
| 2558 | ); | 2700 | ); |
| 2559 | 2701 | ||
| 2560 | const baseSetupConfig = persistedTargets[0]!.setupConfig; | ||
| 2561 | const unionScopes = normalizeScopes( | ||
| 2562 | persistedTargets.flatMap((target) => [...target.requiredScopes]), | ||
| 2563 | ); | ||
| 2564 | const workspaceOauthClient = yield* resolveWorkspaceOauthClientById({ | 2702 | const workspaceOauthClient = yield* resolveWorkspaceOauthClientById({ |
| 2565 | rows: input.rows, | 2703 | rows: input.rows, |
| 2566 | workspaceId: input.sourceInput.workspaceId, | 2704 | workspaceId: input.sourceInput.workspaceId, |
| 2567 | oauthClientId: input.sourceInput.workspaceOauthClientId, | 2705 | oauthClientId: input.sourceInput.workspaceOauthClientId, |
| 2568 | providerKey: baseSetupConfig.providerKey, | 2706 | providerKey: persistedTargets[0]!.setupConfig.providerKey, |
| 2569 | }); | 2707 | }); |
| 2570 | if (workspaceOauthClient === null) { | 2708 | if (workspaceOauthClient === null) { |
| 2571 | return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`); | 2709 | return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`); |
| 2572 | } | 2710 | } |
| 2573 | 2711 | ||
| 2574 | const reusableGrant = yield* findReusableProviderGrant({ | 2712 | const providerAuthResult = yield* connectSourcesWithProviderRuntimeAuth({ |
| 2575 | rows: input.rows, | 2713 | rows: input.rows, |
| 2714 | sourceStore: input.sourceStore, | ||
| 2715 | sourceCatalogSync: input.sourceCatalogSync, | ||
| 2576 | workspaceId: input.sourceInput.workspaceId, | 2716 | workspaceId: input.sourceInput.workspaceId, |
| 2577 | actorAccountId: input.sourceInput.actorAccountId, | 2717 | actorAccountId: input.sourceInput.actorAccountId, |
| 2578 | providerKey: baseSetupConfig.providerKey, | 2718 | executionId: input.sourceInput.executionId, |
| 2579 | oauthClientId: workspaceOauthClient.id, | 2719 | interactionId: input.sourceInput.interactionId, |
| 2580 | requiredScopes: unionScopes, | 2720 | baseUrl: input.sourceInput.baseUrl, |
| 2721 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, | ||
| 2722 | workspaceOauthClient, | ||
| 2723 | targets: persistedTargets, | ||
| 2581 | }); | 2724 | }); |
| 2582 | 2725 | ||
| 2583 | if (Option.isSome(reusableGrant)) { | 2726 | if (providerAuthResult.kind === "connected") { |
| 2584 | const connectedSources = yield* attachProviderGrantToSources({ | ||
| 2585 | sourceStore: input.sourceStore, | ||
| 2586 | sourceCatalogSync: input.sourceCatalogSync, | ||
| 2587 | actorAccountId: input.sourceInput.actorAccountId, | ||
| 2588 | grantId: reusableGrant.value.id, | ||
| 2589 | providerKey: reusableGrant.value.providerKey, | ||
| 2590 | headerName: reusableGrant.value.headerName, | ||
| 2591 | prefix: reusableGrant.value.prefix, | ||
| 2592 | targets: persistedTargets.map((target) => ({ | ||
| 2593 | source: target.source, | ||
| 2594 | requiredScopes: target.requiredScopes, | ||
| 2595 | })), | ||
| 2596 | }); | ||
| 2597 | |||
| 2598 | return { | 2727 | return { |
| 2599 | results: connectedSources.map((source) => ({ | 2728 | results: providerAuthResult.sources.map((source) => ({ |
| 2600 | source, | 2729 | source, |
| 2601 | status: "connected" as const, | 2730 | status: "connected" as const, |
| 2602 | })), | 2731 | })), |
| @@ -2604,51 +2733,15 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 1 unmodified line | |||
| 2604 | }; | 2733 | }; |
| 2605 | } | 2734 | } |
| 2606 | 2735 | ||
| 2607 | const baseUrl = trimOrNull(input.sourceInput.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null; | ||
| 2608 | if (baseUrl === null) { | ||
| 2609 | return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup"); | ||
| 2610 | } | ||
| 2611 | |||
| 2612 | const oauthRequired = yield* startProviderOauthBatchCredentialSetup({ | ||
| 2613 | rows: input.rows, | ||
| 2614 | sourceStore: input.sourceStore, | ||
| 2615 | workspaceId: input.sourceInput.workspaceId, | ||
| 2616 | actorAccountId: input.sourceInput.actorAccountId, | ||
| 2617 | executionId: input.sourceInput.executionId, | ||
| 2618 | interactionId: input.sourceInput.interactionId, | ||
| 2619 | baseUrl, | ||
| 2620 | redirectModeOverride: trimOrNull(input.sourceInput.baseUrl) ? "app_callback" : undefined, | ||
| 2621 | workspaceOauthClient, | ||
| 2622 | setupConfig: { | ||
| 2623 | ...baseSetupConfig, | ||
| 2624 | scopes: unionScopes, | ||
| 2625 | }, | ||
| 2626 | targetSources: persistedTargets.map((target) => ({ | ||
| 2627 | source: target.source, | ||
| 2628 | requiredScopes: target.requiredScopes, | ||
| 2629 | })), | ||
| 2630 | }); | ||
| 2631 | |||
| 2632 | const pendingSources = yield* Effect.forEach( | ||
| 2633 | persistedTargets, | ||
| 2634 | (target) => | ||
| 2635 | input.sourceStore.loadSourceById({ | ||
| 2636 | workspaceId: input.sourceInput.workspaceId, | ||
| 2637 | sourceId: target.source.id, | ||
| 2638 | actorAccountId: input.sourceInput.actorAccountId, | ||
| 2639 | }), | ||
| 2640 | { discard: false }, | ||
| 2641 | ); | ||
| 2642 | |||
| 2643 | return { | 2736 | return { |
| 2644 | results: pendingSources.map((source) => ({ | 2737 | results: providerAuthResult.sources.map((source) => ({ |
| 2645 | source, | 2738 | source, |
| 2646 | status: "pending_oauth" as const, | 2739 | status: "pending_oauth" as const, |
| 2647 | })), | 2740 | })), |
| 2648 | providerOauthSession: { | 2741 | providerOauthSession: { |
| 2649 | sessionId: oauthRequired.sessionId, | 2742 | sessionId: providerAuthResult.sessionId, |
| 2650 | authorizationUrl: oauthRequired.authorizationUrl, | 2743 | authorizationUrl: providerAuthResult.authorizationUrl, |
| 2651 | sourceIds: pendingSources.map((source) => source.id), | 2744 | sourceIds: providerAuthResult.sources.map((source) => source.id), |
| 2652 | }, | 2745 | }, |
| 2653 | }; | 2746 | }; |
| 2654 | }); | 2747 | }); |
| @@ -2883,15 +2976,14 @@ const createRuntimeSourceConnectionService = ( · 228 unmodified lines | |||
| 2883 | createWorkspaceOauthClient: ({ workspaceId, providerKey, label, oauthClient }) => | 2976 | createWorkspaceOauthClient: ({ workspaceId, providerKey, label, oauthClient }) => |
| 2884 | provideLocalWorkspace( | 2977 | provideLocalWorkspace( |
| 2885 | Effect.gen(function* () { | 2978 | Effect.gen(function* () { |
| 2979 | const sourceAdapter = findSourceAdapterByProviderKey(providerKey); | ||
| 2886 | const created = yield* createWorkspaceOauthClient({ | 2980 | const created = yield* createWorkspaceOauthClient({ |
| 2887 | rows: input.rows, | 2981 | rows: input.rows, |
| 2888 | workspaceId, | 2982 | workspaceId, |
| 2889 | providerKey, | 2983 | providerKey, |
| 2890 | oauthClient, | 2984 | oauthClient, |
| 2891 | label, | 2985 | label, |
| 2892 | normalizeOauthClient: providerKey === "google_workspace" | 2986 | normalizeOauthClient: sourceAdapter?.normalizeOauthClientInput, |
| 2893 | ? getSourceAdapter("google_discovery").normalizeOauthClientInput | ||
| 2894 | : undefined, | ||
| 2895 | storeSecretMaterial: input.storeSecretMaterial, | 2987 | storeSecretMaterial: input.storeSecretMaterial, |
| 2896 | }); | 2988 | }); |
| 2897 | const stored = yield* input.rows.workspaceOauthClients.getById(created.id); | 2989 | const stored = yield* input.rows.workspaceOauthClients.getById(created.id); |
| @@ -2,75 +2,52 @@ import * as Effect from "effect/Effect"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | fallbackSourceDiscoveryResult, | 4 | fallbackSourceDiscoveryResult, |
| 5 | looksLikeGraphqlEndpoint, | ||
| 6 | normalizeSourceDiscoveryUrl, | 5 | normalizeSourceDiscoveryUrl, |
| 7 | probeHeadersFromAuth, | 6 | probeHeadersFromAuth, |
| 7 | type SourceAdapter, | ||
| 8 | type SourceProbeAuth, | 8 | type SourceProbeAuth, |
| 9 | type SourceDiscoveryResult, | 9 | type SourceDiscoveryResult, |
| 10 | } from "@executor/source-core"; | 10 | } from "@executor/source-core"; |
| 11 | import { detectGoogleDiscoverySource } from "@executor/source-google-discovery"; | 11 | |
| 12 | import { detectGraphqlSource } from "@executor/source-graphql"; | 12 | import { builtInSourceAdapters } from "./source-adapters"; |
| 13 | import { detectMcpSource } from "@executor/source-mcp"; | 13 | |
| 14 | import { detectOpenApiSource } from "@executor/source-openapi"; | 14 | type DiscoverableSourceAdapter = SourceAdapter & { |
| 15 | detectSource: NonNullable<SourceAdapter["detectSource"]>; | ||
| 16 | }; | ||
| 17 | |||
| 18 | const sourceAdapters: ReadonlyArray<SourceAdapter> = builtInSourceAdapters; | ||
| 19 | |||
| 20 | const discoverableSourceAdapters = sourceAdapters.filter( | ||
| 21 | (adapter): adapter is DiscoverableSourceAdapter => | ||
| 22 | adapter.detectSource !== undefined, | ||
| 23 | ); | ||
| 15 | 24 | ||
| 16 | export const discoverSource = (input: { | 25 | export const discoverSource = (input: { |
| 17 | url: string; | 26 | url: string; |
| 18 | probeAuth?: SourceProbeAuth | null; | 27 | probeAuth?: SourceProbeAuth | null; |
| 19 | }): Effect.Effect<SourceDiscoveryResult, Error, never> => | 28 | }): Effect.Effect<SourceDiscoveryResult, Error, never> => |
| 20 | Effect.gen(function* () { | 29 | Effect.gen(function* () { |
| 21 | const normalizedUrl = normalizeSourceDiscoveryUrl(input.url); | 30 | const normalizedUrl = yield* Effect.try({ |
| 31 | try: () => normalizeSourceDiscoveryUrl(input.url), | ||
| 32 | catch: (cause) => | ||
| 33 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 34 | }); | ||
| 22 | const headers = probeHeadersFromAuth(input.probeAuth); | 35 | const headers = probeHeadersFromAuth(input.probeAuth); |
| 23 | 36 | ||
| 24 | if (looksLikeGraphqlEndpoint(normalizedUrl)) { | 37 | const adaptersByPriority = [...discoverableSourceAdapters].sort( |
| 25 | const graphql = yield* detectGraphqlSource({ | 38 | (left, right) => |
| 26 | normalizedUrl, | 39 | (right.discoveryPriority?.({ normalizedUrl }) ?? 0) |
| 27 | headers, | 40 | - (left.discoveryPriority?.({ normalizedUrl }) ?? 0), |
| 28 | }); | 41 | ); |
| 29 | if (graphql) { | ||
| 30 | return graphql; | ||
| 31 | } | ||
| 32 | 42 | ||
| 33 | const mcp = yield* detectMcpSource({ | 43 | for (const adapter of adaptersByPriority) { |
| 44 | const detected = yield* adapter.detectSource({ | ||
| 34 | normalizedUrl, | 45 | normalizedUrl, |
| 35 | headers, | 46 | headers, |
| 36 | }); | 47 | }); |
| 37 | if (mcp) { | 48 | if (detected) { |
| 38 | return mcp; | 49 | return detected; |
| 39 | } | 50 | } |
| 40 | |||
| 41 | return fallbackSourceDiscoveryResult(normalizedUrl); | ||
| 42 | } | ||
| 43 | |||
| 44 | const googleDiscovery = yield* detectGoogleDiscoverySource({ | ||
| 45 | normalizedUrl, | ||
| 46 | headers, | ||
| 47 | }); | ||
| 48 | if (googleDiscovery) { | ||
| 49 | return googleDiscovery; | ||
| 50 | } | ||
| 51 | |||
| 52 | const openApi = yield* detectOpenApiSource({ | ||
| 53 | normalizedUrl, | ||
| 54 | headers, | ||
| 55 | }); | ||
| 56 | if (openApi) { | ||
| 57 | return openApi; | ||
| 58 | } | ||
| 59 | |||
| 60 | const graphql = yield* detectGraphqlSource({ | ||
| 61 | normalizedUrl, | ||
| 62 | headers, | ||
| 63 | }); | ||
| 64 | if (graphql) { | ||
| 65 | return graphql; | ||
| 66 | } | ||
| 67 | |||
| 68 | const mcp = yield* detectMcpSource({ | ||
| 69 | normalizedUrl, | ||
| 70 | headers, | ||
| 71 | }); | ||
| 72 | if (mcp) { | ||
| 73 | return mcp; | ||
| 74 | } | 51 | } |
| 75 | 52 | ||
| 76 | return fallbackSourceDiscoveryResult(normalizedUrl); | 53 | return fallbackSourceDiscoveryResult(normalizedUrl); |
| @@ -12,6 +12,7 @@ import { · 11 unmodified lines | |||
| 12 | fromConfigSecretProviderId, | 12 | fromConfigSecretProviderId, |
| 13 | toConfigSecretProviderId, | 13 | toConfigSecretProviderId, |
| 14 | } from "../../local/config-secrets"; | 14 | } from "../../local/config-secrets"; |
| 15 | import { getSourceAdapterForSource } from "../source-adapters"; | ||
| 15 | import { slugify } from "../slug"; | 16 | import { slugify } from "../slug"; |
| 16 | 17 | ||
| 17 | export const trimOrNull = (value: string | null | undefined): string | null => { | 18 | export const trimOrNull = (value: string | null | undefined): string | null => { |
| @@ -168,47 +169,17 @@ export const configSourceFromLocalSource = (input: { · 150 unmodified lines | |||
| 168 | }, | 169 | }, |
| 169 | }; | 170 | }; |
| 170 | 171 | ||
| 171 | switch (input.source.kind) { | 172 | const adapter = getSourceAdapterForSource(input.source); |
| 172 | case "openapi": | 173 | if (adapter.localConfigBindingSchema === null) { |
| 173 | return { | 174 | throw new LocalUnsupportedSourceKindError({ |
| 174 | kind: "openapi", | 175 | message: `Unsupported source kind for local config: ${input.source.kind}`, |
| 175 | ...common, | 176 | kind: input.source.kind, |
| 176 | binding: cloneJson(input.source.binding) as Extract< | 177 | }); |
| 177 | LocalConfigSource, | ||
| 178 | { kind: "openapi" } | ||
| 179 | >["binding"], | ||
| 180 | }; | ||
| 181 | case "graphql": | ||
| 182 | return { | ||
| 183 | kind: "graphql", | ||
| 184 | ...common, | ||
| 185 | binding: cloneJson(input.source.binding) as Extract< | ||
| 186 | LocalConfigSource, | ||
| 187 | { kind: "graphql" } | ||
| 188 | >["binding"], | ||
| 189 | }; | ||
| 190 | case "google_discovery": | ||
| 191 | return { | ||
| 192 | kind: "google_discovery", | ||
| 193 | ...common, | ||
| 194 | binding: cloneJson(input.source.binding) as Extract< | ||
| 195 | LocalConfigSource, | ||
| 196 | { kind: "google_discovery" } | ||
| 197 | >["binding"], | ||
| 198 | }; | ||
| 199 | case "mcp": | ||
| 200 | return { | ||
| 201 | kind: "mcp", | ||
| 202 | ...common, | ||
| 203 | binding: cloneJson(input.source.binding) as Extract< | ||
| 204 | LocalConfigSource, | ||
| 205 | { kind: "mcp" } | ||
| 206 | >["binding"], | ||
| 207 | }; | ||
| 208 | default: | ||
| 209 | throw new LocalUnsupportedSourceKindError({ | ||
| 210 | message: `Unsupported source kind for local config: ${input.source.kind}`, | ||
| 211 | kind: input.source.kind, | ||
| 212 | }); | ||
| 213 | } | 178 | } |
| 179 | |||
| 180 | return { | ||
| 181 | kind: input.source.kind as LocalConfigSource["kind"], | ||
| 182 | ...common, | ||
| 183 | binding: cloneJson(adapter.localConfigBindingFromSource(input.source)), | ||
| 184 | } as LocalConfigSource; | ||
| 214 | }; | 185 | }; |
| @@ -1,11 +1,6 @@ | |||
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { localConfigurableSourceAdapters } from "../../runtime/sources/source-adapters"; |
| 4 | GoogleDiscoveryLocalConfigBindingSchema, | ||
| 5 | } from "@executor/source-google-discovery"; | ||
| 6 | import { GraphqlLocalConfigBindingSchema } from "@executor/source-graphql"; | ||
| 7 | import { McpLocalConfigBindingSchema } from "@executor/source-mcp"; | ||
| 8 | import { OpenApiLocalConfigBindingSchema } from "@executor/source-openapi"; | ||
| 9 | 4 | ||
| 10 | export const LocalExecutorRuntimeSchema = Schema.Literal( | 5 | export const LocalExecutorRuntimeSchema = Schema.Literal( |
| 11 | "quickjs", | 6 | "quickjs", |
| @@ -73,44 +68,44 @@ const LocalConfigSourceEntryBaseSchema = Schema.Struct({ · 61 unmodified lines | |||
| 73 | connection: LocalConfigSourceConnectionSchema, | 68 | connection: LocalConfigSourceConnectionSchema, |
| 74 | }); | 69 | }); |
| 75 | 70 | ||
| 76 | export const LocalConfigOpenApiSourceSchema = Schema.extend( | 71 | type SchemaTypeOf<TSchema> = TSchema extends { readonly Type: infer T } ? T : never; |
| 77 | LocalConfigSourceEntryBaseSchema, | 72 | type LocalConfigSourceEntryBase = typeof LocalConfigSourceEntryBaseSchema.Type; |
| 78 | Schema.Struct({ | 73 | |
| 79 | kind: Schema.Literal("openapi"), | 74 | type LocalConfigSourceFromAdapter< |
| 80 | binding: OpenApiLocalConfigBindingSchema, | 75 | TAdapter extends (typeof localConfigurableSourceAdapters)[number], |
| 81 | }), | 76 | > = |
| 82 | ); | 77 | TAdapter extends { |
| 83 | 78 | key: infer TKey extends string; | |
| 84 | export const LocalConfigGraphqlSourceSchema = Schema.extend( | 79 | localConfigBindingSchema: infer TSchema; |
| 85 | LocalConfigSourceEntryBaseSchema, | 80 | } |
| 86 | Schema.Struct({ | 81 | ? { |
| 87 | kind: Schema.Literal("graphql"), | 82 | kind: TKey; |
| 88 | binding: GraphqlLocalConfigBindingSchema, | 83 | binding: SchemaTypeOf<TSchema>; |
| 89 | }), | 84 | } & LocalConfigSourceEntryBase |
| 90 | ); | 85 | : never; |
| 91 | 86 | ||
| 92 | export const LocalConfigMcpSourceSchema = Schema.extend( | 87 | export type LocalConfigSource = LocalConfigSourceFromAdapter< |
| 93 | LocalConfigSourceEntryBaseSchema, | 88 | (typeof localConfigurableSourceAdapters)[number] |
| 94 | Schema.Struct({ | 89 | >; |
| 95 | kind: Schema.Literal("mcp"), | 90 | |
| 96 | binding: McpLocalConfigBindingSchema, | 91 | const LocalConfigSourceEntrySchemas = localConfigurableSourceAdapters.map( |
| 97 | }), | 92 | (adapter) => |
| 98 | ); | 93 | Schema.extend( |
| 99 | 94 | LocalConfigSourceEntryBaseSchema, | |
| 100 | export const LocalConfigGoogleDiscoverySourceSchema = Schema.extend( | 95 | Schema.Struct({ |
| 101 | LocalConfigSourceEntryBaseSchema, | 96 | kind: Schema.Literal(adapter.key as LocalConfigSource["kind"]), |
| 102 | Schema.Struct({ | 97 | binding: adapter.localConfigBindingSchema, |
| 103 | kind: Schema.Literal("google_discovery"), | 98 | }), |
| 104 | binding: GoogleDiscoveryLocalConfigBindingSchema, | 99 | ), |
| 105 | }), | ||
| 106 | ); | 100 | ); |
| 107 | 101 | ||
| 108 | export const LocalConfigSourceSchema = Schema.Union( | 102 | export const LocalConfigSourceSchema = Schema.Union( |
| 109 | LocalConfigOpenApiSourceSchema, | 103 | ...((LocalConfigSourceEntrySchemas as unknown) as [ |
| 110 | LocalConfigGraphqlSourceSchema, | 104 | Schema.Schema<any, any, never>, |
| 111 | LocalConfigMcpSourceSchema, | 105 | Schema.Schema<any, any, never>, |
| 112 | LocalConfigGoogleDiscoverySourceSchema, | 106 | ...Array<Schema.Schema<any, any, never>>, |
| 113 | ); | 107 | ]), |
| 108 | ) as Schema.Schema<LocalConfigSource, any, never>; | ||
| 114 | 109 | ||
| 115 | export const LocalConfigPolicyActionSchema = Schema.Literal("allow", "deny"); | 110 | export const LocalConfigPolicyActionSchema = Schema.Literal("allow", "deny"); |
| 116 | export const LocalConfigPolicyApprovalSchema = Schema.Literal("auto", "manual"); | 111 | export const LocalConfigPolicyApprovalSchema = Schema.Literal("auto", "manual"); |
| @@ -168,7 +163,6 @@ export type LocalConfigSecretProvider = · 51 unmodified lines | |||
| 168 | export type LocalConfigExplicitSecretRef = | 163 | export type LocalConfigExplicitSecretRef = |
| 169 | typeof LocalConfigExplicitSecretRefSchema.Type; | 164 | typeof LocalConfigExplicitSecretRefSchema.Type; |
| 170 | export type LocalConfigSecretInput = typeof LocalConfigSecretInputSchema.Type; | 165 | export type LocalConfigSecretInput = typeof LocalConfigSecretInputSchema.Type; |
| 171 | export type LocalConfigSource = typeof LocalConfigSourceSchema.Type; | ||
| 172 | export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type; | 166 | export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type; |
| 173 | export type LocalConfigSecrets = typeof LocalConfigSecretsSchema.Type; | 167 | export type LocalConfigSecrets = typeof LocalConfigSecretsSchema.Type; |
| 174 | export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type; | 168 | export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type; |
| @@ -0,0 +1,26 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/source-builtins", | ||
| 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 | "@executor/source-core": "workspace:*", | ||
| 16 | "@executor/source-google-discovery": "workspace:*", | ||
| 17 | "@executor/source-graphql": "workspace:*", | ||
| 18 | "@executor/source-mcp": "workspace:*", | ||
| 19 | "@executor/source-openapi": "workspace:*" | ||
| 20 | }, | ||
| 21 | "devDependencies": { | ||
| 22 | "@types/node": "catalog:", | ||
| 23 | "bun-types": "catalog:" | ||
| 24 | }, | ||
| 25 | "version": "0.0.0" | ||
| 26 | } | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | import type { SourceAdapter } from "@executor/source-core"; | ||
| 2 | import { googleDiscoverySourceAdapter } from "@executor/source-google-discovery"; | ||
| 3 | import { graphqlSourceAdapter } from "@executor/source-graphql"; | ||
| 4 | import { mcpSourceAdapter } from "@executor/source-mcp"; | ||
| 5 | import { openApiSourceAdapter } from "@executor/source-openapi"; | ||
| 6 | |||
| 7 | export const externalSourceAdapters = [ | ||
| 8 | openApiSourceAdapter, | ||
| 9 | graphqlSourceAdapter, | ||
| 10 | googleDiscoverySourceAdapter, | ||
| 11 | mcpSourceAdapter, | ||
| 12 | ] as const satisfies readonly SourceAdapter[]; | ||
| @@ -0,0 +1,15 @@ | |||
| 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 | ] | ||
| 15 | } | ||
| @@ -0,0 +1,88 @@ | |||
| 1 | import * as Schema from "effect/Schema"; | ||
| 2 | |||
| 3 | import { createSourceAdapterRegistry } from "./registry"; | ||
| 4 | import type { SourceAdapter, SourceAdapterInputSchema } from "./types"; | ||
| 5 | |||
| 6 | type SchemaTypeOf<TSchema> = TSchema extends { readonly Type: infer T } ? T : never; | ||
| 7 | type AdapterUnion<TAdapters extends readonly SourceAdapter[]> = TAdapters[number]; | ||
| 8 | type ConnectPayloadSchema<TAdapters extends readonly SourceAdapter[]> = | ||
| 9 | NonNullable<AdapterUnion<TAdapters>["connectPayloadSchema"]>; | ||
| 10 | type ExecutorAddInputSchema<TAdapters extends readonly SourceAdapter[]> = | ||
| 11 | NonNullable<AdapterUnion<TAdapters>["executorAddInputSchema"]>; | ||
| 12 | |||
| 13 | export type ConnectableSourceAdapter<TAdapter extends SourceAdapter = SourceAdapter> = TAdapter & { | ||
| 14 | connectPayloadSchema: SourceAdapterInputSchema; | ||
| 15 | }; | ||
| 16 | |||
| 17 | export type ExecutorAddableSourceAdapter<TAdapter extends SourceAdapter = SourceAdapter> = TAdapter & { | ||
| 18 | executorAddInputSchema: SourceAdapterInputSchema; | ||
| 19 | }; | ||
| 20 | |||
| 21 | export type LocalConfigurableSourceAdapter<TAdapter extends SourceAdapter = SourceAdapter> = TAdapter & { | ||
| 22 | localConfigBindingSchema: SourceAdapterInputSchema; | ||
| 23 | }; | ||
| 24 | |||
| 25 | const isConnectableSourceAdapter = <TAdapter extends SourceAdapter>( | ||
| 26 | adapter: TAdapter, | ||
| 27 | ): adapter is ConnectableSourceAdapter<TAdapter> => | ||
| 28 | adapter.connectPayloadSchema !== null; | ||
| 29 | |||
| 30 | const isExecutorAddableSourceAdapter = <TAdapter extends SourceAdapter>( | ||
| 31 | adapter: TAdapter, | ||
| 32 | ): adapter is ExecutorAddableSourceAdapter<TAdapter> => | ||
| 33 | adapter.executorAddInputSchema !== null; | ||
| 34 | |||
| 35 | const isLocalConfigurableSourceAdapter = <TAdapter extends SourceAdapter>( | ||
| 36 | adapter: TAdapter, | ||
| 37 | ): adapter is LocalConfigurableSourceAdapter<TAdapter> => | ||
| 38 | adapter.localConfigBindingSchema !== null; | ||
| 39 | |||
| 40 | const asSchemaTuple = ( | ||
| 41 | schemas: ReadonlyArray<Schema.Schema<any, any, never>>, | ||
| 42 | ): [ | ||
| 43 | Schema.Schema<any, any, never>, | ||
| 44 | Schema.Schema<any, any, never>, | ||
| 45 | ...Array<Schema.Schema<any, any, never>>, | ||
| 46 | ] => schemas as [ | ||
| 47 | Schema.Schema<any, any, never>, | ||
| 48 | Schema.Schema<any, any, never>, | ||
| 49 | ...Array<Schema.Schema<any, any, never>>, | ||
| 50 | ]; | ||
| 51 | |||
| 52 | const createSchemaFromAdapters = <TSchema extends SourceAdapterInputSchema>( | ||
| 53 | schemas: ReadonlyArray<TSchema>, | ||
| 54 | label: string, | ||
| 55 | ): TSchema => | ||
| 56 | (schemas.length === 0 | ||
| 57 | ? (() => { | ||
| 58 | throw new Error(`Cannot create ${label} without any schemas`); | ||
| 59 | })() | ||
| 60 | : schemas.length === 1 | ||
| 61 | ? schemas[0]! | ||
| 62 | : Schema.Union(...asSchemaTuple(schemas))) as TSchema; | ||
| 63 | |||
| 64 | export const createSourceAdapterComposition = <const TAdapters extends readonly SourceAdapter[]>( | ||
| 65 | adapters: TAdapters, | ||
| 66 | ) => { | ||
| 67 | const connectableSourceAdapters = adapters.filter(isConnectableSourceAdapter); | ||
| 68 | const executorAddableSourceAdapters = adapters.filter(isExecutorAddableSourceAdapter); | ||
| 69 | const localConfigurableSourceAdapters = adapters.filter(isLocalConfigurableSourceAdapter); | ||
| 70 | const registry = createSourceAdapterRegistry(adapters); | ||
| 71 | |||
| 72 | return { | ||
| 73 | connectableSourceAdapters, | ||
| 74 | connectPayloadSchema: createSchemaFromAdapters( | ||
| 75 | connectableSourceAdapters.map((adapter) => adapter.connectPayloadSchema), | ||
| 76 | "connect payload schema", | ||
| 77 | ) as Schema.Schema<SchemaTypeOf<ConnectPayloadSchema<TAdapters>>, any, never>, | ||
| 78 | executorAddableSourceAdapters, | ||
| 79 | executorAddInputSchema: createSchemaFromAdapters( | ||
| 80 | executorAddableSourceAdapters.map((adapter) => adapter.executorAddInputSchema), | ||
| 81 | "executor add input schema", | ||
| 82 | ) as Schema.Schema<SchemaTypeOf<ExecutorAddInputSchema<TAdapters>>, any, never>, | ||
| 83 | localConfigurableSourceAdapters, | ||
| 84 | ...registry, | ||
| 85 | }; | ||
| 86 | }; | ||
| 87 | |||
| 88 | export type SourceAdapterComposition = ReturnType<typeof createSourceAdapterComposition>; | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | export * from "./catalog"; | 1 | export * from "./catalog"; |
| 2 | export * from "./catalog-sync-result"; | 2 | export * from "./catalog-sync-result"; |
| 3 | export * from "./composition"; | ||
| 3 | export * from "./discovery"; | 4 | export * from "./discovery"; |
| 4 | export * from "./discovery-models"; | 5 | export * from "./discovery-models"; |
| 5 | export * from "./effect-errors"; | 6 | export * from "./effect-errors"; |
| @@ -20,10 +20,14 @@ export const createSourceAdapterRegistry = <const TAdapters extends readonly Sou · 19 unmodified lines | |||
| 20 | const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceAdapter => | 20 | const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceAdapter => |
| 21 | getSourceAdapter(source.kind); | 21 | getSourceAdapter(source.kind); |
| 22 | 22 | ||
| 23 | const findSourceAdapterByProviderKey = (providerKey: string): SourceAdapter | null => | ||
| 24 | adapters.find((adapter) => adapter.providerKey === providerKey) ?? null; | ||
| 25 | |||
| 23 | return { | 26 | return { |
| 24 | adapters, | 27 | adapters, |
| 25 | getSourceAdapter, | 28 | getSourceAdapter, |
| 26 | getSourceAdapterForSource, | 29 | getSourceAdapterForSource, |
| 30 | findSourceAdapterByProviderKey, | ||
| 27 | sourceBindingStateFromSource: (source: Source) => | 31 | sourceBindingStateFromSource: (source: Source) => |
| 28 | getSourceAdapterForSource(source).bindingStateFromSource(source), | 32 | getSourceAdapterForSource(source).bindingStateFromSource(source), |
| 29 | sourceAdapterCatalogKind: (key: string): SourceCatalogKind => | 33 | sourceAdapterCatalogKind: (key: string): SourceCatalogKind => |
| @@ -9,6 +9,7 @@ import type { Schema } from "effect"; · 8 unmodified lines | |||
| 9 | 9 | ||
| 10 | import type { OAuth2ClientAuthenticationMethod } from "@executor/auth-oauth2"; | 10 | import type { OAuth2ClientAuthenticationMethod } from "@executor/auth-oauth2"; |
| 11 | import type { SourceCatalogSyncResult } from "./catalog-sync-result"; | 11 | import type { SourceCatalogSyncResult } from "./catalog-sync-result"; |
| 12 | import type { SourceDiscoveryResult } from "./discovery-models"; | ||
| 12 | import type { | 13 | import type { |
| 13 | CredentialSlot, | 14 | CredentialSlot, |
| 14 | SecretRef, | 15 | SecretRef, |
| @@ -128,6 +129,11 @@ export type SourceAdapterInvokeInput = { · 113 unmodified lines | |||
| 128 | context?: Record<string, unknown>; | 129 | context?: Record<string, unknown>; |
| 129 | }; | 130 | }; |
| 130 | 131 | ||
| 132 | export type SourceAdapterDiscoveryInput = { | ||
| 133 | normalizedUrl: string; | ||
| 134 | headers: Readonly<Record<string, string>>; | ||
| 135 | }; | ||
| 136 | |||
| 131 | export type SourceAdapter = { | 137 | export type SourceAdapter = { |
| 132 | key: string; | 138 | key: string; |
| 133 | displayName: string; | 139 | displayName: string; |
| @@ -141,6 +147,8 @@ export type SourceAdapter = { · 7 unmodified lines | |||
| 141 | executorAddInputSchema: SourceAdapterInputSchema | null; | 147 | executorAddInputSchema: SourceAdapterInputSchema | null; |
| 142 | executorAddHelpText: readonly string[] | null; | 148 | executorAddHelpText: readonly string[] | null; |
| 143 | executorAddInputSignatureWidth: number | null; | 149 | executorAddInputSignatureWidth: number | null; |
| 150 | localConfigBindingSchema: SourceAdapterInputSchema | null; | ||
| 151 | localConfigBindingFromSource: (source: Source) => unknown; | ||
| 144 | serializeBindingConfig: (source: Source) => string; | 152 | serializeBindingConfig: (source: Source) => string; |
| 145 | deserializeBindingConfig: ( | 153 | deserializeBindingConfig: ( |
| 146 | input: Pick<StoredSourceRecord, "id" | "bindingConfigJson">, | 154 | input: Pick<StoredSourceRecord, "id" | "bindingConfigJson">, |
| @@ -149,6 +157,12 @@ export type SourceAdapter = { · 2 unmodified lines | |||
| 149 | sourceConfigFromSource: (source: Source) => Record<string, unknown>; | 157 | sourceConfigFromSource: (source: Source) => Record<string, unknown>; |
| 150 | validateSource: (source: Source) => Effect.Effect<Source, Error, never>; | 158 | validateSource: (source: Source) => Effect.Effect<Source, Error, never>; |
| 151 | shouldAutoProbe: (source: Source) => boolean; | 159 | shouldAutoProbe: (source: Source) => boolean; |
| 160 | discoveryPriority?: (input: { | ||
| 161 | normalizedUrl: string; | ||
| 162 | }) => number; | ||
| 163 | detectSource?: ( | ||
| 164 | input: SourceAdapterDiscoveryInput, | ||
| 165 | ) => Effect.Effect<SourceDiscoveryResult | null, Error, never>; | ||
| 152 | syncCatalog: ( | 166 | syncCatalog: ( |
| 153 | input: SourceAdapterSyncInput, | 167 | input: SourceAdapterSyncInput, |
| 154 | ) => Effect.Effect<SourceCatalogSyncResult, Error, never>; | 168 | ) => Effect.Effect<SourceCatalogSyncResult, Error, never>; |
| @@ -12,11 +12,13 @@ import { · 11 unmodified lines | |||
| 12 | compileGoogleDiscoveryToolDefinitions, | 12 | compileGoogleDiscoveryToolDefinitions, |
| 13 | extractGoogleDiscoveryManifest, | 13 | extractGoogleDiscoveryManifest, |
| 14 | } from "./document"; | 14 | } from "./document"; |
| 15 | import { detectGoogleDiscoverySource } from "./discovery"; | ||
| 15 | import { buildGoogleDiscoveryToolPresentation } from "./tools"; | 16 | import { buildGoogleDiscoveryToolPresentation } from "./tools"; |
| 16 | import { | 17 | import { |
| 17 | createGoogleDiscoveryCatalogFragment, | 18 | createGoogleDiscoveryCatalogFragment, |
| 18 | type GoogleDiscoveryCatalogOperationInput, | 19 | type GoogleDiscoveryCatalogOperationInput, |
| 19 | } from "./catalog"; | 20 | } from "./catalog"; |
| 21 | import { GoogleDiscoveryLocalConfigBindingSchema } from "./local-config"; | ||
| 20 | import { | 22 | import { |
| 21 | GoogleDiscoveryToolProviderDataSchema, | 23 | GoogleDiscoveryToolProviderDataSchema, |
| 22 | type GoogleDiscoveryToolManifest, | 24 | type GoogleDiscoveryToolManifest, |
| @@ -424,7 +426,7 @@ const googleDiscoveryOauth2SetupConfig = (source: Source) => · 401 unmodified lines | |||
| 424 | }; | 426 | }; |
| 425 | }); | 427 | }); |
| 426 | 428 | ||
| 427 | export const googleDiscoverySourceAdapter: SourceAdapter = { | 429 | export const googleDiscoverySourceAdapter = { |
| 428 | key: "google_discovery", | 430 | key: "google_discovery", |
| 429 | displayName: "Google Discovery", | 431 | displayName: "Google Discovery", |
| 430 | catalogKind: "imported", | 432 | catalogKind: "imported", |
| @@ -439,6 +441,20 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 8 unmodified lines | |||
| 439 | "service is the Discovery service name, e.g. sheets or drive. version is the API version, e.g. v4 or v3.", | 441 | "service is the Discovery service name, e.g. sheets or drive. version is the API version, e.g. v4 or v3.", |
| 440 | ], | 442 | ], |
| 441 | executorAddInputSignatureWidth: 420, | 443 | executorAddInputSignatureWidth: 420, |
| 444 | localConfigBindingSchema: GoogleDiscoveryLocalConfigBindingSchema, | ||
| 445 | localConfigBindingFromSource: (source) => | ||
| 446 | Effect.runSync( | ||
| 447 | Effect.map( | ||
| 448 | googleDiscoveryBindingConfigFromSource(source), | ||
| 449 | (bindingConfig) => ({ | ||
| 450 | service: bindingConfig.service, | ||
| 451 | version: bindingConfig.version, | ||
| 452 | discoveryUrl: bindingConfig.discoveryUrl, | ||
| 453 | defaultHeaders: bindingConfig.defaultHeaders ?? null, | ||
| 454 | scopes: bindingConfig.scopes, | ||
| 455 | }), | ||
| 456 | ), | ||
| 457 | ), | ||
| 442 | serializeBindingConfig: (source) => | 458 | serializeBindingConfig: (source) => |
| 443 | encodeBindingConfig({ | 459 | encodeBindingConfig({ |
| 444 | adapterKey: "google_discovery", | 460 | adapterKey: "google_discovery", |
| @@ -508,6 +524,15 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 63 unmodified lines | |||
| 508 | shouldAutoProbe: (source) => | 524 | shouldAutoProbe: (source) => |
| 509 | source.enabled && | 525 | source.enabled && |
| 510 | (source.status === "draft" || source.status === "probing"), | 526 | (source.status === "draft" || source.status === "probing"), |
| 527 | discoveryPriority: ({ normalizedUrl }) => | ||
| 528 | normalizedUrl.includes("$discovery/rest") || normalizedUrl.includes("/discovery/v1/apis/") | ||
| 529 | ? 500 | ||
| 530 | : 300, | ||
| 531 | detectSource: ({ normalizedUrl, headers }) => | ||
| 532 | detectGoogleDiscoverySource({ | ||
| 533 | normalizedUrl, | ||
| 534 | headers, | ||
| 535 | }), | ||
| 511 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | 536 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 512 | Effect.gen(function* () { | 537 | Effect.gen(function* () { |
| 513 | const bindingConfig = | 538 | const bindingConfig = |
| @@ -684,4 +709,4 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 170 unmodified lines | |||
| 684 | catch: (cause) => | 709 | catch: (cause) => |
| 685 | cause instanceof Error ? cause : new Error(String(cause)), | 710 | cause instanceof Error ? cause : new Error(String(cause)), |
| 686 | }), | 711 | }), |
| 687 | }; | 712 | } satisfies SourceAdapter; |
| @@ -7,6 +7,7 @@ import { · 6 unmodified lines | |||
| 7 | GraphqlToolProviderDataSchema, | 7 | GraphqlToolProviderDataSchema, |
| 8 | type GraphqlToolProviderData, | 8 | type GraphqlToolProviderData, |
| 9 | } from "./provider-data"; | 9 | } from "./provider-data"; |
| 10 | import { detectGraphqlSource } from "./discovery"; | ||
| 10 | import { | 11 | import { |
| 11 | createGraphqlCatalogFragment, | 12 | createGraphqlCatalogFragment, |
| 12 | type GraphqlCatalogOperationInput, | 13 | type GraphqlCatalogOperationInput, |
| @@ -17,6 +18,7 @@ import { · 4 unmodified lines | |||
| 17 | extractGraphqlManifest, | 18 | extractGraphqlManifest, |
| 18 | GRAPHQL_INTROSPECTION_QUERY, | 19 | GRAPHQL_INTROSPECTION_QUERY, |
| 19 | } from "./graphql-tools"; | 20 | } from "./graphql-tools"; |
| 21 | import { GraphqlLocalConfigBindingSchema } from "./local-config"; | ||
| 20 | import { | 22 | import { |
| 21 | ConnectHttpAuthSchema, | 23 | ConnectHttpAuthSchema, |
| 22 | ConnectHttpImportAuthSchema, | 24 | ConnectHttpImportAuthSchema, |
| @@ -27,6 +29,7 @@ import { · 4 unmodified lines | |||
| 27 | emptySourceBindingState, | 29 | emptySourceBindingState, |
| 28 | encodeBindingConfig, | 30 | encodeBindingConfig, |
| 29 | isSourceCredentialRequiredError, | 31 | isSourceCredentialRequiredError, |
| 32 | looksLikeGraphqlEndpoint, | ||
| 30 | OptionalNullableStringSchema, | 33 | OptionalNullableStringSchema, |
| 31 | SourceCredentialRequiredError, | 34 | SourceCredentialRequiredError, |
| 32 | SourceConnectCommonFieldsSchema, | 35 | SourceConnectCommonFieldsSchema, |
| @@ -255,7 +258,7 @@ const withoutUndefinedEntries = ( · 222 unmodified lines | |||
| 255 | Object.entries(record).filter(([, value]) => value !== undefined), | 258 | Object.entries(record).filter(([, value]) => value !== undefined), |
| 256 | ); | 259 | ); |
| 257 | 260 | ||
| 258 | export const graphqlSourceAdapter: SourceAdapter = { | 261 | export const graphqlSourceAdapter = { |
| 259 | key: "graphql", | 262 | key: "graphql", |
| 260 | displayName: "GraphQL", | 263 | displayName: "GraphQL", |
| 261 | catalogKind: "imported", | 264 | catalogKind: "imported", |
| @@ -268,6 +271,13 @@ export const graphqlSourceAdapter: SourceAdapter = { · 6 unmodified lines | |||
| 268 | executorAddInputSchema: GraphqlExecutorAddInputSchema, | 271 | executorAddInputSchema: GraphqlExecutorAddInputSchema, |
| 269 | executorAddHelpText: ["endpoint is the GraphQL HTTP endpoint."], | 272 | executorAddHelpText: ["endpoint is the GraphQL HTTP endpoint."], |
| 270 | executorAddInputSignatureWidth: 320, | 273 | executorAddInputSignatureWidth: 320, |
| 274 | localConfigBindingSchema: GraphqlLocalConfigBindingSchema, | ||
| 275 | localConfigBindingFromSource: (source) => | ||
| 276 | Effect.runSync( | ||
| 277 | Effect.map(graphqlBindingConfigFromSource(source), (bindingConfig) => ({ | ||
| 278 | defaultHeaders: bindingConfig.defaultHeaders, | ||
| 279 | })), | ||
| 280 | ), | ||
| 271 | serializeBindingConfig: (source) => | 281 | serializeBindingConfig: (source) => |
| 272 | encodeBindingConfig({ | 282 | encodeBindingConfig({ |
| 273 | adapterKey: "graphql", | 283 | adapterKey: "graphql", |
| @@ -318,6 +328,13 @@ export const graphqlSourceAdapter: SourceAdapter = { · 44 unmodified lines | |||
| 318 | shouldAutoProbe: (source) => | 328 | shouldAutoProbe: (source) => |
| 319 | source.enabled && | 329 | source.enabled && |
| 320 | (source.status === "draft" || source.status === "probing"), | 330 | (source.status === "draft" || source.status === "probing"), |
| 331 | discoveryPriority: ({ normalizedUrl }) => | ||
| 332 | looksLikeGraphqlEndpoint(normalizedUrl) ? 400 : 150, | ||
| 333 | detectSource: ({ normalizedUrl, headers }) => | ||
| 334 | detectGraphqlSource({ | ||
| 335 | normalizedUrl, | ||
| 336 | headers, | ||
| 337 | }), | ||
| 321 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | 338 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 322 | Effect.gen(function* () { | 339 | Effect.gen(function* () { |
| 323 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); | 340 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); |
| @@ -519,4 +536,4 @@ export const graphqlSourceAdapter: SourceAdapter = { · 195 unmodified lines | |||
| 519 | catch: (cause) => | 536 | catch: (cause) => |
| 520 | cause instanceof Error ? cause : new Error(String(cause)), | 537 | cause instanceof Error ? cause : new Error(String(cause)), |
| 521 | }), | 538 | }), |
| 522 | }; | 539 | } satisfies SourceAdapter; |
| @@ -12,10 +12,12 @@ import { · 11 unmodified lines | |||
| 12 | type McpToolManifest, | 12 | type McpToolManifest, |
| 13 | type McpToolManifestEntry, | 13 | type McpToolManifestEntry, |
| 14 | } from "./tools"; | 14 | } from "./tools"; |
| 15 | import { detectMcpSource } from "./discovery"; | ||
| 15 | import { | 16 | import { |
| 16 | createMcpCatalogFragment, | 17 | createMcpCatalogFragment, |
| 17 | type McpCatalogOperationInput, | 18 | type McpCatalogOperationInput, |
| 18 | } from "./catalog"; | 19 | } from "./catalog"; |
| 20 | import { McpLocalConfigBindingSchema } from "./local-config"; | ||
| 19 | import type { McpServerMetadata } from "./manifest"; | 21 | import type { McpServerMetadata } from "./manifest"; |
| 20 | import { | 22 | import { |
| 21 | contentHash, | 23 | contentHash, |
| @@ -25,6 +27,7 @@ import { · 3 unmodified lines | |||
| 25 | decodeSourceBindingPayload, | 27 | decodeSourceBindingPayload, |
| 26 | emptySourceBindingState, | 28 | emptySourceBindingState, |
| 27 | encodeBindingConfig, | 29 | encodeBindingConfig, |
| 30 | looksLikeGraphqlEndpoint, | ||
| 28 | McpConnectFieldsSchema, | 31 | McpConnectFieldsSchema, |
| 29 | OptionalNullableStringSchema, | 32 | OptionalNullableStringSchema, |
| 30 | SourceTransportSchema, | 33 | SourceTransportSchema, |
| @@ -326,7 +329,7 @@ export const catalogSyncResultFromMcpManifest = (input: { · 295 unmodified lines | |||
| 326 | }); | 329 | }); |
| 327 | }; | 330 | }; |
| 328 | 331 | ||
| 329 | export const mcpSourceAdapter: SourceAdapter = { | 332 | export const mcpSourceAdapter = { |
| 330 | key: "mcp", | 333 | key: "mcp", |
| 331 | displayName: "MCP", | 334 | displayName: "MCP", |
| 332 | catalogKind: "imported", | 335 | catalogKind: "imported", |
| @@ -342,6 +345,19 @@ export const mcpSourceAdapter: SourceAdapter = { · 9 unmodified lines | |||
| 342 | 'For local servers, set transport: "stdio" and provide command plus optional args/env/cwd.', | 345 | 'For local servers, set transport: "stdio" and provide command plus optional args/env/cwd.', |
| 343 | ], | 346 | ], |
| 344 | executorAddInputSignatureWidth: 240, | 347 | executorAddInputSignatureWidth: 240, |
| 348 | localConfigBindingSchema: McpLocalConfigBindingSchema, | ||
| 349 | localConfigBindingFromSource: (source) => | ||
| 350 | Effect.runSync( | ||
| 351 | Effect.map(mcpBindingConfigFromSource(source), (bindingConfig) => ({ | ||
| 352 | transport: bindingConfig.transport, | ||
| 353 | queryParams: bindingConfig.queryParams, | ||
| 354 | headers: bindingConfig.headers, | ||
| 355 | command: bindingConfig.command, | ||
| 356 | args: bindingConfig.args, | ||
| 357 | env: bindingConfig.env, | ||
| 358 | cwd: bindingConfig.cwd, | ||
| 359 | })), | ||
| 360 | ), | ||
| 345 | serializeBindingConfig: (source) => | 361 | serializeBindingConfig: (source) => |
| 346 | encodeBindingConfig({ | 362 | encodeBindingConfig({ |
| 347 | adapterKey: "mcp", | 363 | adapterKey: "mcp", |
| @@ -408,6 +424,13 @@ export const mcpSourceAdapter: SourceAdapter = { · 60 unmodified lines | |||
| 408 | }; | 424 | }; |
| 409 | }), | 425 | }), |
| 410 | shouldAutoProbe: () => false, | 426 | shouldAutoProbe: () => false, |
| 427 | discoveryPriority: ({ normalizedUrl }) => | ||
| 428 | looksLikeGraphqlEndpoint(normalizedUrl) ? 350 : 125, | ||
| 429 | detectSource: ({ normalizedUrl, headers }) => | ||
| 430 | detectMcpSource({ | ||
| 431 | normalizedUrl, | ||
| 432 | headers, | ||
| 433 | }), | ||
| 411 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | 434 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 412 | Effect.gen(function* () { | 435 | Effect.gen(function* () { |
| 413 | const bindingConfig = yield* mcpBindingConfigFromSource(source); | 436 | const bindingConfig = yield* mcpBindingConfigFromSource(source); |
| @@ -599,4 +622,4 @@ export const mcpSourceAdapter: SourceAdapter = { · 185 unmodified lines | |||
| 599 | status: null, | 622 | status: null, |
| 600 | }; | 623 | }; |
| 601 | }), | 624 | }), |
| 602 | }; | 625 | } satisfies SourceAdapter; |
| @@ -10,12 +10,14 @@ import { · 9 unmodified lines | |||
| 10 | } from "@executor/codemode-core"; | 10 | } from "@executor/codemode-core"; |
| 11 | import { buildOpenApiToolPresentation } from "./tool-presentation"; | 11 | import { buildOpenApiToolPresentation } from "./tool-presentation"; |
| 12 | import { compileOpenApiToolDefinitions } from "./definitions"; | 12 | import { compileOpenApiToolDefinitions } from "./definitions"; |
| 13 | import { detectOpenApiSource } from "./discovery"; | ||
| 13 | import { | 14 | import { |
| 14 | httpBodyModeFromContentType, | 15 | httpBodyModeFromContentType, |
| 15 | serializeOpenApiParameterValue, | 16 | serializeOpenApiParameterValue, |
| 16 | serializeOpenApiRequestBody, | 17 | serializeOpenApiRequestBody, |
| 17 | withSerializedQueryEntries, | 18 | withSerializedQueryEntries, |
| 18 | } from "./http-serialization"; | 19 | } from "./http-serialization"; |
| 20 | import { OpenApiLocalConfigBindingSchema } from "./local-config"; | ||
| 19 | import { | 21 | import { |
| 20 | OpenApiToolProviderDataSchema, | 22 | OpenApiToolProviderDataSchema, |
| 21 | type OpenApiRefHintTable, | 23 | type OpenApiRefHintTable, |
| @@ -36,6 +38,7 @@ import { · 14 unmodified lines | |||
| 36 | emptySourceBindingState, | 38 | emptySourceBindingState, |
| 37 | encodeBindingConfig, | 39 | encodeBindingConfig, |
| 38 | isSourceCredentialRequiredError, | 40 | isSourceCredentialRequiredError, |
| 41 | looksLikeGraphqlEndpoint, | ||
| 39 | OptionalNullableStringSchema, | 42 | OptionalNullableStringSchema, |
| 40 | SourceCredentialRequiredError, | 43 | SourceCredentialRequiredError, |
| 41 | SourceConnectCommonFieldsSchema, | 44 | SourceConnectCommonFieldsSchema, |
| @@ -402,7 +405,7 @@ const openApiCatalogOperationFromDefinition = (input: { · 360 unmodified lines | |||
| 402 | }; | 405 | }; |
| 403 | }; | 406 | }; |
| 404 | 407 | ||
| 405 | export const openApiSourceAdapter: SourceAdapter = { | 408 | export const openApiSourceAdapter = { |
| 406 | key: "openapi", | 409 | key: "openapi", |
| 407 | displayName: "OpenAPI", | 410 | displayName: "OpenAPI", |
| 408 | catalogKind: "imported", | 411 | catalogKind: "imported", |
| @@ -417,6 +420,14 @@ export const openApiSourceAdapter: SourceAdapter = { · 8 unmodified lines | |||
| 417 | "endpoint is the base API URL. specUrl is the OpenAPI document URL.", | 420 | "endpoint is the base API URL. specUrl is the OpenAPI document URL.", |
| 418 | ], | 421 | ], |
| 419 | executorAddInputSignatureWidth: 420, | 422 | executorAddInputSignatureWidth: 420, |
| 423 | localConfigBindingSchema: OpenApiLocalConfigBindingSchema, | ||
| 424 | localConfigBindingFromSource: (source) => | ||
| 425 | Effect.runSync( | ||
| 426 | Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({ | ||
| 427 | specUrl: bindingConfig.specUrl, | ||
| 428 | defaultHeaders: bindingConfig.defaultHeaders, | ||
| 429 | })), | ||
| 430 | ), | ||
| 420 | serializeBindingConfig: (source) => | 431 | serializeBindingConfig: (source) => |
| 421 | encodeBindingConfig({ | 432 | encodeBindingConfig({ |
| 422 | adapterKey: "openapi", | 433 | adapterKey: "openapi", |
| @@ -472,6 +483,13 @@ export const openApiSourceAdapter: SourceAdapter = { · 49 unmodified lines | |||
| 472 | shouldAutoProbe: (source) => | 483 | shouldAutoProbe: (source) => |
| 473 | source.enabled && | 484 | source.enabled && |
| 474 | (source.status === "draft" || source.status === "probing"), | 485 | (source.status === "draft" || source.status === "probing"), |
| 486 | discoveryPriority: ({ normalizedUrl }) => | ||
| 487 | looksLikeGraphqlEndpoint(normalizedUrl) ? 50 : 250, | ||
| 488 | detectSource: ({ normalizedUrl, headers }) => | ||
| 489 | detectOpenApiSource({ | ||
| 490 | normalizedUrl, | ||
| 491 | headers, | ||
| 492 | }), | ||
| 475 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | 493 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 476 | Effect.gen(function* () { | 494 | Effect.gen(function* () { |
| 477 | const bindingConfig = yield* openApiBindingConfigFromSource(source); | 495 | const bindingConfig = yield* openApiBindingConfigFromSource(source); |
| @@ -663,4 +681,4 @@ export const openApiSourceAdapter: SourceAdapter = { · 185 unmodified lines | |||
| 663 | catch: (cause) => | 681 | catch: (cause) => |
| 664 | cause instanceof Error ? cause : new Error(String(cause)), | 682 | cause instanceof Error ? cause : new Error(String(cause)), |
| 665 | }), | 683 | }), |
| 666 | }; | 684 | } satisfies SourceAdapter; |