Unified · Split
| @@ -9,9 +9,7 @@ import type { · 8 unmodified lines | |||
| 9 | CreateSecretPayload, | 9 | CreateSecretPayload, |
| 10 | CreateSecretResult, | 10 | CreateSecretResult, |
| 11 | CreateSourcePayload, | 11 | CreateSourcePayload, |
| 12 | CreateWorkspaceOauthClientPayload, | ||
| 13 | DeleteSecretResult, | 12 | DeleteSecretResult, |
| 14 | DiscoverSourcePayload, | ||
| 15 | InstanceConfig, | 13 | InstanceConfig, |
| 16 | LocalInstallation, | 14 | LocalInstallation, |
| 17 | SecretListItem, | 15 | SecretListItem, |
| @@ -21,11 +19,9 @@ import type { · 3 unmodified lines | |||
| 21 | } from "@executor/platform-api"; | 19 | } from "@executor/platform-api"; |
| 22 | import type { | 20 | import type { |
| 23 | Source, | 21 | Source, |
| 24 | SourceDiscoveryResult, | ||
| 25 | SourceInspection, | 22 | SourceInspection, |
| 26 | SourceInspectionDiscoverResult, | 23 | SourceInspectionDiscoverResult, |
| 27 | SourceInspectionToolDetail, | 24 | SourceInspectionToolDetail, |
| 28 | ScopeOauthClient as WorkspaceOauthClient, | ||
| 29 | } from "@executor/platform-sdk/schema"; | 25 | } from "@executor/platform-sdk/schema"; |
| 30 | import * as Cause from "effect/Cause"; | 26 | import * as Cause from "effect/Cause"; |
| 31 | import * as Effect from "effect/Effect"; | 27 | import * as Effect from "effect/Effect"; |
| @@ -67,13 +63,6 @@ type SourceDiscoveryKeyParts = readonly [ · 35 unmodified lines | |||
| 67 | string, | 63 | string, |
| 68 | number | null, | 64 | number | null, |
| 69 | ]; | 65 | ]; |
| 70 | type WorkspaceOauthClientsKeyParts = readonly [ | ||
| 71 | boolean, | ||
| 72 | Source["scopeId"], | ||
| 73 | string, | ||
| 74 | string, | ||
| 75 | ]; | ||
| 76 | |||
| 77 | type InvalidationTarget = { | 66 | type InvalidationTarget = { |
| 78 | workspaceId?: Source["scopeId"]; | 67 | workspaceId?: Source["scopeId"]; |
| 79 | accountId?: string; | 68 | accountId?: string; |
| @@ -83,7 +72,6 @@ type InvalidationTarget = { · 3 unmodified lines | |||
| 83 | type ActiveQueryCollections = { | 72 | type ActiveQueryCollections = { |
| 84 | sourceLists: Set<string>; | 73 | sourceLists: Set<string>; |
| 85 | sources: Set<string>; | 74 | sources: Set<string>; |
| 86 | workspaceOauthClients: Set<string>; | ||
| 87 | inspections: Set<string>; | 75 | inspections: Set<string>; |
| 88 | toolDetails: Set<string>; | 76 | toolDetails: Set<string>; |
| 89 | discoveries: Set<string>; | 77 | discoveries: Set<string>; |
| @@ -171,16 +159,6 @@ const encodeDiscoveryKey = ( · 81 unmodified lines | |||
| 171 | ): string => | 159 | ): string => |
| 172 | encodeAtomKey([enabled, workspaceId, accountId, sourceId, query, limit] satisfies SourceDiscoveryKeyParts); | 160 | encodeAtomKey([enabled, workspaceId, accountId, sourceId, query, limit] satisfies SourceDiscoveryKeyParts); |
| 173 | 161 | ||
| 174 | const encodeWorkspaceOauthClientsKey = ( | ||
| 175 | enabled: boolean, | ||
| 176 | workspaceId: Source["scopeId"], | ||
| 177 | accountId: string, | ||
| 178 | providerKey: string, | ||
| 179 | ): string => | ||
| 180 | encodeAtomKey( | ||
| 181 | [enabled, workspaceId, accountId, providerKey] satisfies WorkspaceOauthClientsKeyParts, | ||
| 182 | ); | ||
| 183 | |||
| 184 | const toError = (cause: unknown): Error => | 162 | const toError = (cause: unknown): Error => |
| 185 | cause instanceof Error ? cause : new Error(String(cause)); | 163 | cause instanceof Error ? cause : new Error(String(cause)); |
| 186 | 164 | ||
| @@ -391,26 +369,6 @@ const sourceDiscoveryAtom = Atom.family((key: string) => { · 204 unmodified lines | |||
| 391 | ).pipe(Atom.keepAlive); | 369 | ).pipe(Atom.keepAlive); |
| 392 | }); | 370 | }); |
| 393 | 371 | ||
| 394 | const workspaceOauthClientsAtom = Atom.family((key: string) => { | ||
| 395 | const [enabled, workspaceId, accountId, providerKey] = decodeAtomKey<WorkspaceOauthClientsKeyParts>(key); | ||
| 396 | |||
| 397 | return Atom.make( | ||
| 398 | enabled | ||
| 399 | ? controlPlaneRequest({ | ||
| 400 | accountId, | ||
| 401 | execute: (client) => client.sources.listWorkspaceOauthClients({ | ||
| 402 | path: { | ||
| 403 | workspaceId, | ||
| 404 | }, | ||
| 405 | urlParams: { | ||
| 406 | providerKey, | ||
| 407 | }, | ||
| 408 | }), | ||
| 409 | }) | ||
| 410 | : Effect.never, | ||
| 411 | ).pipe(Atom.keepAlive); | ||
| 412 | }); | ||
| 413 | |||
| 414 | export type Loadable<T> = | 372 | export type Loadable<T> = |
| 415 | | { status: "loading" } | 373 | | { status: "loading" } |
| 416 | | { status: "error"; error: Error } | 374 | | { status: "error"; error: Error } |
| @@ -539,7 +497,6 @@ const setCachedAtomValue = <A>( · 122 unmodified lines | |||
| 539 | const createActiveQueryCollections = (): ActiveQueryCollections => ({ | 497 | const createActiveQueryCollections = (): ActiveQueryCollections => ({ |
| 540 | sourceLists: new Set(), | 498 | sourceLists: new Set(), |
| 541 | sources: new Set(), | 499 | sources: new Set(), |
| 542 | workspaceOauthClients: new Set(), | ||
| 543 | inspections: new Set(), | 500 | inspections: new Set(), |
| 544 | toolDetails: new Set(), | 501 | toolDetails: new Set(), |
| 545 | discoveries: new Set(), | 502 | discoveries: new Set(), |
| @@ -582,13 +539,6 @@ const invalidateTrackedQueries = ( · 36 unmodified lines | |||
| 582 | } | 539 | } |
| 583 | }); | 540 | }); |
| 584 | 541 | ||
| 585 | activeQueries.workspaceOauthClients.forEach((key) => { | ||
| 586 | const [enabled, workspaceId, accountId] = decodeAtomKey<WorkspaceOauthClientsKeyParts>(key); | ||
| 587 | if (enabled && targetMatches(target, workspaceId, accountId)) { | ||
| 588 | registry.refresh(workspaceOauthClientsAtom(key)); | ||
| 589 | } | ||
| 590 | }); | ||
| 591 | |||
| 592 | activeQueries.inspections.forEach((key) => { | 542 | activeQueries.inspections.forEach((key) => { |
| 593 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceKeyParts>(key); | 543 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceKeyParts>(key); |
| 594 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { | 544 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { |
| @@ -946,37 +896,6 @@ export const useSource = (sourceId: string): Loadable<Source> => { · 351 unmodified lines | |||
| 946 | return workspace.enabled ? source : pendingLoadable(workspace.workspace); | 896 | return workspace.enabled ? source : pendingLoadable(workspace.workspace); |
| 947 | }; | 897 | }; |
| 948 | 898 | ||
| 949 | export const useWorkspaceOauthClients = ( | ||
| 950 | providerKey: string | null, | ||
| 951 | ): Loadable<ReadonlyArray<WorkspaceOauthClient>> => { | ||
| 952 | const workspace = useWorkspaceRequestContext(); | ||
| 953 | const key = encodeWorkspaceOauthClientsKey( | ||
| 954 | workspace.enabled && providerKey !== null, | ||
| 955 | workspace.workspaceId, | ||
| 956 | workspace.accountId, | ||
| 957 | providerKey ?? "", | ||
| 958 | ); | ||
| 959 | useTrackActiveKey( | ||
| 960 | "workspaceOauthClients", | ||
| 961 | key, | ||
| 962 | workspace.enabled && providerKey !== null, | ||
| 963 | ); | ||
| 964 | const oauthClients = useLoadableAtom(workspaceOauthClientsAtom(key)); | ||
| 965 | |||
| 966 | if (!workspace.enabled) { | ||
| 967 | return pendingLoadable(workspace.workspace); | ||
| 968 | } | ||
| 969 | |||
| 970 | if (providerKey === null) { | ||
| 971 | return { | ||
| 972 | status: "ready", | ||
| 973 | data: [], | ||
| 974 | }; | ||
| 975 | } | ||
| 976 | |||
| 977 | return oauthClients; | ||
| 978 | }; | ||
| 979 | |||
| 980 | export const useSourceInspection = (sourceId: string): Loadable<SourceInspection> => { | 899 | export const useSourceInspection = (sourceId: string): Loadable<SourceInspection> => { |
| 981 | const workspace = useWorkspaceRequestContext(); | 900 | const workspace = useWorkspaceRequestContext(); |
| 982 | const requestedSourceId = workspace.enabled | 901 | const requestedSourceId = workspace.enabled |
| @@ -1214,116 +1133,20 @@ export const useRemoveSource = () => · 231 unmodified lines | |||
| 1214 | }, | 1133 | }, |
| 1215 | ); | 1134 | ); |
| 1216 | 1135 | ||
| 1217 | export const useDiscoverSource = () => | ||
| 1218 | useSourceMutation<DiscoverSourcePayload, SourceDiscoveryResult>( | ||
| 1219 | React.useCallback( | ||
| 1220 | ({ accountId, payload }) => | ||
| 1221 | runControlPlane({ | ||
| 1222 | accountId, | ||
| 1223 | execute: (client) => client.sources.discover({ | ||
| 1224 | payload, | ||
| 1225 | }), | ||
| 1226 | }), | ||
| 1227 | [], | ||
| 1228 | ), | ||
| 1229 | ); | ||
| 1230 | |||
| 1231 | export const useCreateWorkspaceOauthClient = () => | ||
| 1232 | useSourceMutation<CreateWorkspaceOauthClientPayload, WorkspaceOauthClient>( | ||
| 1233 | React.useCallback( | ||
| 1234 | ({ workspaceId, accountId, payload }) => | ||
| 1235 | runControlPlane({ | ||
| 1236 | accountId, | ||
| 1237 | execute: (client) => client.sources.createWorkspaceOauthClient({ | ||
| 1238 | path: { | ||
| 1239 | workspaceId, | ||
| 1240 | }, | ||
| 1241 | payload, | ||
| 1242 | }), | ||
| 1243 | }), | ||
| 1244 | [], | ||
| 1245 | ), | ||
| 1246 | { | ||
| 1247 | onSuccess: (context) => { | ||
| 1248 | context.invalidateQueries({ | ||
| 1249 | workspaceId: context.workspaceId, | ||
| 1250 | accountId: context.accountId, | ||
| 1251 | }); | ||
| 1252 | }, | ||
| 1253 | }, | ||
| 1254 | ); | ||
| 1255 | |||
| 1256 | export const useRemoveWorkspaceOauthClient = () => | ||
| 1257 | useSourceMutation<WorkspaceOauthClient["id"], { removed: boolean }>( | ||
| 1258 | React.useCallback( | ||
| 1259 | ({ workspaceId, accountId, payload }) => | ||
| 1260 | runControlPlane({ | ||
| 1261 | accountId, | ||
| 1262 | execute: (client) => client.sources.removeWorkspaceOauthClient({ | ||
| 1263 | path: { | ||
| 1264 | workspaceId, | ||
| 1265 | oauthClientId: payload, | ||
| 1266 | }, | ||
| 1267 | }), | ||
| 1268 | }), | ||
| 1269 | [], | ||
| 1270 | ), | ||
| 1271 | { | ||
| 1272 | onSuccess: (context) => { | ||
| 1273 | context.invalidateQueries({ | ||
| 1274 | workspaceId: context.workspaceId, | ||
| 1275 | accountId: context.accountId, | ||
| 1276 | }); | ||
| 1277 | }, | ||
| 1278 | }, | ||
| 1279 | ); | ||
| 1280 | |||
| 1281 | export const useRemoveProviderAuthGrant = () => | ||
| 1282 | useSourceMutation< | ||
| 1283 | Extract<Source["auth"], { kind: "provider_grant_ref" }>["grantId"], | ||
| 1284 | { removed: boolean } | ||
| 1285 | >( | ||
| 1286 | React.useCallback( | ||
| 1287 | ({ workspaceId, accountId, payload }) => | ||
| 1288 | runControlPlane({ | ||
| 1289 | accountId, | ||
| 1290 | execute: (client) => client.sources.removeProviderAuthGrant({ | ||
| 1291 | path: { | ||
| 1292 | workspaceId, | ||
| 1293 | grantId: payload, | ||
| 1294 | }, | ||
| 1295 | }), | ||
| 1296 | }), | ||
| 1297 | [], | ||
| 1298 | ), | ||
| 1299 | { | ||
| 1300 | onSuccess: (context) => { | ||
| 1301 | context.invalidateQueries({ | ||
| 1302 | workspaceId: context.workspaceId, | ||
| 1303 | accountId: context.accountId, | ||
| 1304 | }); | ||
| 1305 | }, | ||
| 1306 | }, | ||
| 1307 | ); | ||
| 1308 | |||
| 1309 | export type { | 1136 | export type { |
| 1310 | CreateSecretPayload, | 1137 | CreateSecretPayload, |
| 1311 | CreateSecretResult, | 1138 | CreateSecretResult, |
| 1312 | CreateSourcePayload, | 1139 | CreateSourcePayload, |
| 1313 | CreateWorkspaceOauthClientPayload, | ||
| 1314 | DeleteSecretResult, | 1140 | DeleteSecretResult, |
| 1315 | DiscoverSourcePayload, | ||
| 1316 | InstanceConfig, | 1141 | InstanceConfig, |
| 1317 | LocalInstallation, | 1142 | LocalInstallation, |
| 1318 | 1143 | ||
| 1319 | SecretListItem, | 1144 | SecretListItem, |
| 1320 | Source, | 1145 | Source, |
| 1321 | SourceDiscoveryResult, | ||
| 1322 | SourceInspection, | 1146 | SourceInspection, |
| 1323 | SourceInspectionDiscoverResult, | 1147 | SourceInspectionDiscoverResult, |
| 1324 | SourceInspectionToolDetail, | 1148 | SourceInspectionToolDetail, |
| 1325 | UpdateSecretPayload, | 1149 | UpdateSecretPayload, |
| 1326 | UpdateSecretResult, | 1150 | UpdateSecretResult, |
| 1327 | UpdateSourcePayload, | 1151 | UpdateSourcePayload, |
| 1328 | WorkspaceOauthClient, | ||
| 1329 | }; | 1152 | }; |
| @@ -12,8 +12,6 @@ · 11 unmodified lines | |||
| 12 | "./local/api": "./src/local/api.ts", | 12 | "./local/api": "./src/local/api.ts", |
| 13 | "./local/http": "./src/local/http.ts", | 13 | "./local/http": "./src/local/http.ts", |
| 14 | "./local-context": "./src/local-context.ts", | 14 | "./local-context": "./src/local-context.ts", |
| 15 | "./oauth/api": "./src/oauth/api.ts", | ||
| 16 | "./oauth/http": "./src/oauth/http.ts", | ||
| 17 | "./policies/api": "./src/policies/api.ts", | 15 | "./policies/api": "./src/policies/api.ts", |
| 18 | "./policies/http": "./src/policies/http.ts", | 16 | "./policies/http": "./src/policies/http.ts", |
| 19 | "./sources/api": "./src/sources/api.ts", | 17 | "./sources/api": "./src/sources/api.ts", |
| @@ -2,13 +2,11 @@ import { HttpApi, OpenApi } from "@effect/platform"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { ExecutionsApi } from "./executions/api"; | 3 | import { ExecutionsApi } from "./executions/api"; |
| 4 | import { LocalApi } from "./local/api"; | 4 | import { LocalApi } from "./local/api"; |
| 5 | import { OAuthApi } from "./oauth/api"; | ||
| 6 | import { PoliciesApi } from "./policies/api"; | 5 | import { PoliciesApi } from "./policies/api"; |
| 7 | import { SourcesApi } from "./sources/api"; | 6 | import { SourcesApi } from "./sources/api"; |
| 8 | 7 | ||
| 9 | export class ExecutorApi extends HttpApi.make("executor") | 8 | export class ExecutorApi extends HttpApi.make("executor") |
| 10 | .add(LocalApi) | 9 | .add(LocalApi) |
| 11 | .add(OAuthApi) | ||
| 12 | .add(SourcesApi) | 10 | .add(SourcesApi) |
| 13 | .add(PoliciesApi) | 11 | .add(PoliciesApi) |
| 14 | .add(ExecutionsApi) | 12 | .add(ExecutionsApi) |
| @@ -34,19 +34,6 @@ export type ExecutorApiClient = { · 33 unmodified lines | |||
| 34 | remove: PromiseMethod<ExecutorApiEffectClient["policies"]["remove"]>; | 34 | remove: PromiseMethod<ExecutorApiEffectClient["policies"]["remove"]>; |
| 35 | }; | 35 | }; |
| 36 | sources: { | 36 | sources: { |
| 37 | discover: PromiseMethod<ExecutorApiEffectClient["sources"]["discover"]>; | ||
| 38 | listWorkspaceOauthClients: PromiseMethod< | ||
| 39 | ExecutorApiEffectClient["sources"]["listWorkspaceOauthClients"] | ||
| 40 | >; | ||
| 41 | createWorkspaceOauthClient: PromiseMethod< | ||
| 42 | ExecutorApiEffectClient["sources"]["createWorkspaceOauthClient"] | ||
| 43 | >; | ||
| 44 | removeWorkspaceOauthClient: PromiseMethod< | ||
| 45 | ExecutorApiEffectClient["sources"]["removeWorkspaceOauthClient"] | ||
| 46 | >; | ||
| 47 | removeProviderAuthGrant: PromiseMethod< | ||
| 48 | ExecutorApiEffectClient["sources"]["removeProviderAuthGrant"] | ||
| 49 | >; | ||
| 50 | list: PromiseMethod<ExecutorApiEffectClient["sources"]["list"]>; | 37 | list: PromiseMethod<ExecutorApiEffectClient["sources"]["list"]>; |
| 51 | create: PromiseMethod<ExecutorApiEffectClient["sources"]["create"]>; | 38 | create: PromiseMethod<ExecutorApiEffectClient["sources"]["create"]>; |
| 52 | get: PromiseMethod<ExecutorApiEffectClient["sources"]["get"]>; | 39 | get: PromiseMethod<ExecutorApiEffectClient["sources"]["get"]>; |
| @@ -59,16 +46,6 @@ export type ExecutorApiClient = { · 6 unmodified lines | |||
| 59 | >; | 46 | >; |
| 60 | credentialPage: PromiseMethod<ExecutorApiEffectClient["sources"]["credentialPage"]>; | 47 | credentialPage: PromiseMethod<ExecutorApiEffectClient["sources"]["credentialPage"]>; |
| 61 | credentialSubmit: PromiseMethod<ExecutorApiEffectClient["sources"]["credentialSubmit"]>; | 48 | credentialSubmit: PromiseMethod<ExecutorApiEffectClient["sources"]["credentialSubmit"]>; |
| 62 | credentialComplete: PromiseMethod< | ||
| 63 | ExecutorApiEffectClient["sources"]["credentialComplete"] | ||
| 64 | >; | ||
| 65 | providerOauthComplete: PromiseMethod< | ||
| 66 | ExecutorApiEffectClient["sources"]["providerOauthComplete"] | ||
| 67 | >; | ||
| 68 | }; | ||
| 69 | oauth: { | ||
| 70 | startSourceAuth: PromiseMethod<ExecutorApiEffectClient["oauth"]["startSourceAuth"]>; | ||
| 71 | sourceAuthCallback: PromiseMethod<ExecutorApiEffectClient["oauth"]["sourceAuthCallback"]>; | ||
| 72 | }; | 49 | }; |
| 73 | executions: { | 50 | executions: { |
| 74 | create: PromiseMethod<ExecutorApiEffectClient["executions"]["create"]>; | 51 | create: PromiseMethod<ExecutorApiEffectClient["executions"]["create"]>; |
| @@ -125,23 +102,6 @@ export const createExecutorApiClient = async (input: { · 50 unmodified lines | |||
| 125 | remove: wrapMethod(effectClientPromise, (client) => client.policies.remove), | 102 | remove: wrapMethod(effectClientPromise, (client) => client.policies.remove), |
| 126 | }, | 103 | }, |
| 127 | sources: { | 104 | sources: { |
| 128 | discover: wrapMethod(effectClientPromise, (client) => client.sources.discover), | ||
| 129 | listWorkspaceOauthClients: wrapMethod( | ||
| 130 | effectClientPromise, | ||
| 131 | (client) => client.sources.listWorkspaceOauthClients, | ||
| 132 | ), | ||
| 133 | createWorkspaceOauthClient: wrapMethod( | ||
| 134 | effectClientPromise, | ||
| 135 | (client) => client.sources.createWorkspaceOauthClient, | ||
| 136 | ), | ||
| 137 | removeWorkspaceOauthClient: wrapMethod( | ||
| 138 | effectClientPromise, | ||
| 139 | (client) => client.sources.removeWorkspaceOauthClient, | ||
| 140 | ), | ||
| 141 | removeProviderAuthGrant: wrapMethod( | ||
| 142 | effectClientPromise, | ||
| 143 | (client) => client.sources.removeProviderAuthGrant, | ||
| 144 | ), | ||
| 145 | list: wrapMethod(effectClientPromise, (client) => client.sources.list), | 105 | list: wrapMethod(effectClientPromise, (client) => client.sources.list), |
| 146 | create: wrapMethod(effectClientPromise, (client) => client.sources.create), | 106 | create: wrapMethod(effectClientPromise, (client) => client.sources.create), |
| 147 | get: wrapMethod(effectClientPromise, (client) => client.sources.get), | 107 | get: wrapMethod(effectClientPromise, (client) => client.sources.get), |
| @@ -164,24 +124,6 @@ export const createExecutorApiClient = async (input: { · 16 unmodified lines | |||
| 164 | effectClientPromise, | 124 | effectClientPromise, |
| 165 | (client) => client.sources.credentialSubmit, | 125 | (client) => client.sources.credentialSubmit, |
| 166 | ), | 126 | ), |
| 167 | credentialComplete: wrapMethod( | ||
| 168 | effectClientPromise, | ||
| 169 | (client) => client.sources.credentialComplete, | ||
| 170 | ), | ||
| 171 | providerOauthComplete: wrapMethod( | ||
| 172 | effectClientPromise, | ||
| 173 | (client) => client.sources.providerOauthComplete, | ||
| 174 | ), | ||
| 175 | }, | ||
| 176 | oauth: { | ||
| 177 | startSourceAuth: wrapMethod( | ||
| 178 | effectClientPromise, | ||
| 179 | (client) => client.oauth.startSourceAuth, | ||
| 180 | ), | ||
| 181 | sourceAuthCallback: wrapMethod( | ||
| 182 | effectClientPromise, | ||
| 183 | (client) => client.oauth.sourceAuthCallback, | ||
| 184 | ), | ||
| 185 | }, | 127 | }, |
| 186 | executions: { | 128 | executions: { |
| 187 | create: wrapMethod(effectClientPromise, (client) => client.executions.create), | 129 | create: wrapMethod(effectClientPromise, (client) => client.executions.create), |
| @@ -6,13 +6,11 @@ import { ExecutorApi } from "./api"; · 5 unmodified lines | |||
| 6 | import { createControlPlaneExecutorLayer } from "./executor-context"; | 6 | import { createControlPlaneExecutorLayer } from "./executor-context"; |
| 7 | import { ExecutorExecutionsLive } from "./executions/http"; | 7 | import { ExecutorExecutionsLive } from "./executions/http"; |
| 8 | import { ExecutorLocalLive } from "./local/http"; | 8 | import { ExecutorLocalLive } from "./local/http"; |
| 9 | import { ExecutorOAuthLive } from "./oauth/http"; | ||
| 10 | import { ExecutorPoliciesLive } from "./policies/http"; | 9 | import { ExecutorPoliciesLive } from "./policies/http"; |
| 11 | import { ExecutorSourcesLive } from "./sources/http"; | 10 | import { ExecutorSourcesLive } from "./sources/http"; |
| 12 | 11 | ||
| 13 | export const ExecutorApiLive = HttpApiBuilder.api(ExecutorApi).pipe( | 12 | export const ExecutorApiLive = HttpApiBuilder.api(ExecutorApi).pipe( |
| 14 | Layer.provide(ExecutorLocalLive), | 13 | Layer.provide(ExecutorLocalLive), |
| 15 | Layer.provide(ExecutorOAuthLive), | ||
| 16 | Layer.provide(ExecutorSourcesLive), | 14 | Layer.provide(ExecutorSourcesLive), |
| 17 | Layer.provide(ExecutorPoliciesLive), | 15 | Layer.provide(ExecutorPoliciesLive), |
| 18 | Layer.provide(ExecutorExecutionsLive), | 16 | Layer.provide(ExecutorExecutionsLive), |
| @@ -38,28 +38,10 @@ export { · 37 unmodified lines | |||
| 38 | } from "./local/api"; | 38 | } from "./local/api"; |
| 39 | 39 | ||
| 40 | export { | 40 | export { |
| 41 | OAuthApi, | ||
| 42 | StartSourceOAuthPayloadSchema, | ||
| 43 | StartSourceOAuthResultSchema, | ||
| 44 | CompleteSourceOAuthResultSchema, | ||
| 45 | SourceOAuthPopupFailureResultSchema, | ||
| 46 | SourceOAuthPopupResultSchema, | ||
| 47 | SourceOAuthPopupSuccessResultSchema, | ||
| 48 | type StartSourceOAuthPayload, | ||
| 49 | type StartSourceOAuthResult, | ||
| 50 | type CompleteSourceOAuthResult, | ||
| 51 | type SourceOAuthPopupResult, | ||
| 52 | } from "./oauth/api"; | ||
| 53 | |||
| 54 | export { | ||
| 55 | CreateWorkspaceOauthClientPayloadSchema, | ||
| 56 | CreateSourcePayloadSchema, | 41 | CreateSourcePayloadSchema, |
| 57 | DiscoverSourcePayloadSchema, | ||
| 58 | SourcesApi, | 42 | SourcesApi, |
| 59 | UpdateSourcePayloadSchema, | 43 | UpdateSourcePayloadSchema, |
| 60 | type CreateWorkspaceOauthClientPayload, | ||
| 61 | type CreateSourcePayload, | 44 | type CreateSourcePayload, |
| 62 | type DiscoverSourcePayload, | ||
| 63 | type UpdateSourcePayload, | 45 | type UpdateSourcePayload, |
| 64 | } from "./sources/api"; | 46 | } from "./sources/api"; |
| 65 | 47 | ||
| @@ -1,99 +0,0 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | ||
| 2 | import { | ||
| 3 | JsonObjectSchema, | ||
| 4 | SecretRefSchema, | ||
| 5 | SourceAuthSessionIdSchema, | ||
| 6 | ScopeIdSchema as WorkspaceIdSchema, | ||
| 7 | } from "@executor/platform-sdk/schema"; | ||
| 8 | import * as Schema from "effect/Schema"; | ||
| 9 | |||
| 10 | import { | ||
| 11 | ControlPlaneBadRequestError, | ||
| 12 | ControlPlaneForbiddenError, | ||
| 13 | ControlPlaneStorageError, | ||
| 14 | ControlPlaneUnauthorizedError, | ||
| 15 | } from "@executor/platform-sdk/errors"; | ||
| 16 | import { TrimmedNonEmptyStringSchema } from "@executor/platform-sdk/string-schemas"; | ||
| 17 | |||
| 18 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | ||
| 19 | |||
| 20 | const HtmlSchema = HttpApiSchema.Text({ | ||
| 21 | contentType: "text/html", | ||
| 22 | }); | ||
| 23 | |||
| 24 | export const StartSourceOAuthPayloadSchema = Schema.Struct({ | ||
| 25 | providerKey: TrimmedNonEmptyStringSchema, | ||
| 26 | name: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 27 | config: Schema.optional(Schema.NullOr(JsonObjectSchema)), | ||
| 28 | }); | ||
| 29 | |||
| 30 | export type StartSourceOAuthPayload = typeof StartSourceOAuthPayloadSchema.Type; | ||
| 31 | |||
| 32 | export const StartSourceOAuthResultSchema = Schema.Struct({ | ||
| 33 | sessionId: SourceAuthSessionIdSchema, | ||
| 34 | authorizationUrl: Schema.String, | ||
| 35 | }); | ||
| 36 | |||
| 37 | export type StartSourceOAuthResult = typeof StartSourceOAuthResultSchema.Type; | ||
| 38 | |||
| 39 | export const SourceOAuthAuthSchema = Schema.Struct({ | ||
| 40 | kind: Schema.Literal("oauth2"), | ||
| 41 | headerName: Schema.String, | ||
| 42 | prefix: Schema.String, | ||
| 43 | accessToken: SecretRefSchema, | ||
| 44 | refreshToken: Schema.NullOr(SecretRefSchema), | ||
| 45 | }); | ||
| 46 | |||
| 47 | export const CompleteSourceOAuthResultSchema = Schema.Struct({ | ||
| 48 | sessionId: SourceAuthSessionIdSchema, | ||
| 49 | auth: SourceOAuthAuthSchema, | ||
| 50 | }); | ||
| 51 | |||
| 52 | export type CompleteSourceOAuthResult = typeof CompleteSourceOAuthResultSchema.Type; | ||
| 53 | |||
| 54 | export const SourceOAuthPopupSuccessResultSchema = Schema.Struct({ | ||
| 55 | type: Schema.Literal("executor:oauth-result"), | ||
| 56 | ok: Schema.Literal(true), | ||
| 57 | sessionId: SourceAuthSessionIdSchema, | ||
| 58 | auth: SourceOAuthAuthSchema, | ||
| 59 | }); | ||
| 60 | |||
| 61 | export const SourceOAuthPopupFailureResultSchema = Schema.Struct({ | ||
| 62 | type: Schema.Literal("executor:oauth-result"), | ||
| 63 | ok: Schema.Literal(false), | ||
| 64 | sessionId: Schema.Null, | ||
| 65 | error: Schema.String, | ||
| 66 | }); | ||
| 67 | |||
| 68 | export const SourceOAuthPopupResultSchema = Schema.Union( | ||
| 69 | SourceOAuthPopupSuccessResultSchema, | ||
| 70 | SourceOAuthPopupFailureResultSchema, | ||
| 71 | ); | ||
| 72 | |||
| 73 | export type SourceOAuthPopupResult = typeof SourceOAuthPopupResultSchema.Type; | ||
| 74 | |||
| 75 | const OAuthCallbackUrlParamsSchema = Schema.Struct({ | ||
| 76 | state: Schema.String, | ||
| 77 | code: Schema.optional(Schema.String), | ||
| 78 | error: Schema.optional(Schema.String), | ||
| 79 | error_description: Schema.optional(Schema.String), | ||
| 80 | }); | ||
| 81 | |||
| 82 | export class OAuthApi extends HttpApiGroup.make("oauth") | ||
| 83 | .add( | ||
| 84 | HttpApiEndpoint.post("startSourceAuth")`/workspaces/${workspaceIdParam}/oauth/source-auth/start` | ||
| 85 | .setPayload(StartSourceOAuthPayloadSchema) | ||
| 86 | .addSuccess(StartSourceOAuthResultSchema) | ||
| 87 | .addError(ControlPlaneBadRequestError) | ||
| 88 | .addError(ControlPlaneUnauthorizedError) | ||
| 89 | .addError(ControlPlaneForbiddenError) | ||
| 90 | .addError(ControlPlaneStorageError), | ||
| 91 | ) | ||
| 92 | .add( | ||
| 93 | HttpApiEndpoint.get("sourceAuthCallback")`/oauth/source-auth/callback` | ||
| 94 | .setUrlParams(OAuthCallbackUrlParamsSchema) | ||
| 95 | .addSuccess(HtmlSchema) | ||
| 96 | .addError(ControlPlaneBadRequestError) | ||
| 97 | .addError(ControlPlaneStorageError), | ||
| 98 | ) | ||
| 99 | .prefix("/v1") {} | ||
| @@ -1,315 +0,0 @@ | |||
| 1 | import { | ||
| 2 | HttpApiBuilder, | ||
| 3 | HttpServerRequest, | ||
| 4 | HttpServerResponse, | ||
| 5 | } from "@effect/platform"; | ||
| 6 | import type { ScopeId as WorkspaceId } from "@executor/platform-sdk/schema"; | ||
| 7 | import * as Cause from "effect/Cause"; | ||
| 8 | import * as Either from "effect/Either"; | ||
| 9 | import * as Effect from "effect/Effect"; | ||
| 10 | import * as Schema from "effect/Schema"; | ||
| 11 | |||
| 12 | import { ExecutorApi } from "../api"; | ||
| 13 | import { | ||
| 14 | ControlPlaneBadRequestError, | ||
| 15 | ControlPlaneStorageError, | ||
| 16 | } from "../errors"; | ||
| 17 | import { | ||
| 18 | getControlPlaneExecutor, | ||
| 19 | resolveRequestedLocalWorkspace, | ||
| 20 | } from "../local-context"; | ||
| 21 | import { | ||
| 22 | SourceOAuthPopupResultSchema, | ||
| 23 | type SourceOAuthPopupResult, | ||
| 24 | } from "./api"; | ||
| 25 | |||
| 26 | const OAUTH_RESULT_STORAGE_KEY_PREFIX = "executor:oauth-result:"; | ||
| 27 | |||
| 28 | const escapeHtml = (value: string): string => | ||
| 29 | value | ||
| 30 | .replaceAll("&", "&") | ||
| 31 | .replaceAll("<", "<") | ||
| 32 | .replaceAll(">", ">") | ||
| 33 | .replaceAll('"', """) | ||
| 34 | .replaceAll("'", "'"); | ||
| 35 | |||
| 36 | const serializeValueForScript = (value: unknown): string => | ||
| 37 | JSON.stringify(value) | ||
| 38 | .replaceAll("<", "\\u003c") | ||
| 39 | .replaceAll(">", "\\u003e") | ||
| 40 | .replaceAll("&", "\\u0026"); | ||
| 41 | |||
| 42 | const encodeSourceOAuthPopupResultJson = Schema.encodeSync( | ||
| 43 | Schema.parseJson(SourceOAuthPopupResultSchema), | ||
| 44 | ); | ||
| 45 | |||
| 46 | const serializeJsonLiteralForScript = (value: string): string => | ||
| 47 | value | ||
| 48 | .replaceAll("<", "\\u003c") | ||
| 49 | .replaceAll(">", "\\u003e") | ||
| 50 | .replaceAll("&", "\\u0026"); | ||
| 51 | |||
| 52 | const htmlResponse = (html: string, status = 200) => | ||
| 53 | HttpServerResponse.html(html).pipe( | ||
| 54 | HttpServerResponse.setStatus(status), | ||
| 55 | ); | ||
| 56 | |||
| 57 | const readHeader = (headers: unknown, name: string): string | null => { | ||
| 58 | if (headers instanceof Headers) { | ||
| 59 | return headers.get(name); | ||
| 60 | } | ||
| 61 | |||
| 62 | if (headers && typeof headers === "object") { | ||
| 63 | const record = headers as Record<string, unknown>; | ||
| 64 | const direct = record[name]; | ||
| 65 | if (typeof direct === "string" && direct.length > 0) { | ||
| 66 | return direct; | ||
| 67 | } | ||
| 68 | |||
| 69 | const lower = record[name.toLowerCase()]; | ||
| 70 | if (typeof lower === "string" && lower.length > 0) { | ||
| 71 | return lower; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | return null; | ||
| 76 | }; | ||
| 77 | |||
| 78 | const resolveRequestOrigin = (request: { url: string; headers: unknown }): string | null => { | ||
| 79 | try { | ||
| 80 | return new URL(request.url).origin; | ||
| 81 | } catch { | ||
| 82 | const forwardedHost = readHeader(request.headers, "x-forwarded-host"); | ||
| 83 | const host = forwardedHost ?? readHeader(request.headers, "host"); | ||
| 84 | if (!host) { | ||
| 85 | return null; | ||
| 86 | } | ||
| 87 | |||
| 88 | const forwardedProto = readHeader(request.headers, "x-forwarded-proto"); | ||
| 89 | const protocol = forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http"; | ||
| 90 | return `${protocol}://${host}`; | ||
| 91 | } | ||
| 92 | }; | ||
| 93 | |||
| 94 | const toStartSourceOAuthError = (input: { | ||
| 95 | workspaceId: WorkspaceId; | ||
| 96 | providerKey: string; | ||
| 97 | cause: Cause.Cause<unknown>; | ||
| 98 | }) => { | ||
| 99 | const pretty = Cause.pretty(input.cause); | ||
| 100 | console.error("oauth.start_source_auth failed", { | ||
| 101 | workspaceId: input.workspaceId, | ||
| 102 | providerKey: input.providerKey, | ||
| 103 | pretty, | ||
| 104 | }); | ||
| 105 | |||
| 106 | if (pretty.includes("Invalid URL") || pretty.includes("required")) { | ||
| 107 | return new ControlPlaneBadRequestError({ | ||
| 108 | operation: "oauth.start_source_auth", | ||
| 109 | message: pretty, | ||
| 110 | details: pretty, | ||
| 111 | }); | ||
| 112 | } | ||
| 113 | |||
| 114 | return new ControlPlaneStorageError({ | ||
| 115 | operation: "oauth.start_source_auth", | ||
| 116 | message: pretty, | ||
| 117 | details: pretty, | ||
| 118 | }); | ||
| 119 | }; | ||
| 120 | |||
| 121 | const sourceOAuthPopupResultDocument = (input: { | ||
| 122 | title: string; | ||
| 123 | message: string; | ||
| 124 | status: "connected" | "failed"; | ||
| 125 | sessionId: string; | ||
| 126 | payload: SourceOAuthPopupResult; | ||
| 127 | }) => `<!doctype html> | ||
| 128 | <html lang="en"> | ||
| 129 | <head> | ||
| 130 | <meta charset="utf-8" /> | ||
| 131 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 132 | <title>${escapeHtml(input.title)}</title> | ||
| 133 | <style> | ||
| 134 | :root { | ||
| 135 | --bg: #fafaf9; | ||
| 136 | --surface: #ffffff; | ||
| 137 | --text: #1c1917; | ||
| 138 | --text-secondary: #78716c; | ||
| 139 | --border: #e7e5e4; | ||
| 140 | --green: #16a34a; | ||
| 141 | --green-light: #f0fdf4; | ||
| 142 | --green-border: #bbf7d0; | ||
| 143 | --red: #dc2626; | ||
| 144 | --red-light: #fef2f2; | ||
| 145 | --red-border: #fecaca; | ||
| 146 | } | ||
| 147 | |||
| 148 | * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| 149 | |||
| 150 | body { | ||
| 151 | min-height: 100vh; | ||
| 152 | display: flex; | ||
| 153 | align-items: center; | ||
| 154 | justify-content: center; | ||
| 155 | background: var(--bg); | ||
| 156 | color: var(--text); | ||
| 157 | font-family: system-ui, -apple-system, sans-serif; | ||
| 158 | padding: 24px; | ||
| 159 | } | ||
| 160 | |||
| 161 | .card { | ||
| 162 | width: 100%; | ||
| 163 | max-width: 420px; | ||
| 164 | background: var(--surface); | ||
| 165 | border: 1px solid var(--border); | ||
| 166 | border-radius: 16px; | ||
| 167 | padding: 28px; | ||
| 168 | text-align: center; | ||
| 169 | } | ||
| 170 | |||
| 171 | .status { | ||
| 172 | display: inline-flex; | ||
| 173 | align-items: center; | ||
| 174 | justify-content: center; | ||
| 175 | padding: 6px 12px; | ||
| 176 | border-radius: 999px; | ||
| 177 | font-size: 12px; | ||
| 178 | font-weight: 600; | ||
| 179 | margin-bottom: 14px; | ||
| 180 | } | ||
| 181 | |||
| 182 | .status[data-status='connected'] { | ||
| 183 | background: var(--green-light); | ||
| 184 | border: 1px solid var(--green-border); | ||
| 185 | color: var(--green); | ||
| 186 | } | ||
| 187 | |||
| 188 | .status[data-status='failed'] { | ||
| 189 | background: var(--red-light); | ||
| 190 | border: 1px solid var(--red-border); | ||
| 191 | color: var(--red); | ||
| 192 | } | ||
| 193 | |||
| 194 | h1 { | ||
| 195 | font-size: 22px; | ||
| 196 | line-height: 1.2; | ||
| 197 | margin-bottom: 10px; | ||
| 198 | } | ||
| 199 | |||
| 200 | p { | ||
| 201 | color: var(--text-secondary); | ||
| 202 | font-size: 14px; | ||
| 203 | line-height: 1.6; | ||
| 204 | } | ||
| 205 | </style> | ||
| 206 | </head> | ||
| 207 | <body> | ||
| 208 | <main class="card"> | ||
| 209 | <div class="status" data-status="${escapeHtml(input.status)}">${escapeHtml(input.status)}</div> | ||
| 210 | <h1>${escapeHtml(input.title)}</h1> | ||
| 211 | <p>${escapeHtml(input.message)}</p> | ||
| 212 | </main> | ||
| 213 | <script> | ||
| 214 | (() => { | ||
| 215 | const payload = ${serializeJsonLiteralForScript(encodeSourceOAuthPopupResultJson(input.payload))}; | ||
| 216 | const storageKey = ${serializeValueForScript(`${OAUTH_RESULT_STORAGE_KEY_PREFIX}${input.sessionId}`)}; | ||
| 217 | try { | ||
| 218 | window.localStorage.setItem(storageKey, JSON.stringify(payload)); | ||
| 219 | } catch {} | ||
| 220 | try { | ||
| 221 | if (window.opener) { | ||
| 222 | window.opener.postMessage(payload, window.location.origin); | ||
| 223 | } | ||
| 224 | } finally { | ||
| 225 | window.setTimeout(() => window.close(), 120); | ||
| 226 | } | ||
| 227 | })(); | ||
| 228 | </script> | ||
| 229 | </body> | ||
| 230 | </html>`; | ||
| 231 | |||
| 232 | export const ExecutorOAuthLive = HttpApiBuilder.group( | ||
| 233 | ExecutorApi, | ||
| 234 | "oauth", | ||
| 235 | (handlers) => | ||
| 236 | handlers | ||
| 237 | .handle("startSourceAuth", ({ path, payload }) => | ||
| 238 | resolveRequestedLocalWorkspace("oauth.start_source_auth", path.workspaceId).pipe( | ||
| 239 | Effect.flatMap((executor) => | ||
| 240 | Effect.gen(function* () { | ||
| 241 | const request = yield* HttpServerRequest.HttpServerRequest; | ||
| 242 | return yield* executor.oauth.startSourceAuth({ | ||
| 243 | baseUrl: resolveRequestOrigin(request), | ||
| 244 | displayName: payload.name, | ||
| 245 | provider: { | ||
| 246 | providerKey: payload.providerKey, | ||
| 247 | config: payload.config ?? null, | ||
| 248 | }, | ||
| 249 | }); | ||
| 250 | }).pipe( | ||
| 251 | Effect.catchAllCause((cause) => | ||
| 252 | Effect.fail( | ||
| 253 | toStartSourceOAuthError({ | ||
| 254 | workspaceId: path.workspaceId, | ||
| 255 | providerKey: payload.providerKey, | ||
| 256 | cause, | ||
| 257 | }), | ||
| 258 | ), | ||
| 259 | ), | ||
| 260 | ) | ||
| 261 | ), | ||
| 262 | ), | ||
| 263 | ) | ||
| 264 | .handle("sourceAuthCallback", ({ urlParams }) => | ||
| 265 | Effect.flatMap(getControlPlaneExecutor(), (executor) => | ||
| 266 | Effect.gen(function* () { | ||
| 267 | const completed = yield* Effect.either( | ||
| 268 | executor.oauth.completeSourceAuth({ | ||
| 269 | state: urlParams.state, | ||
| 270 | code: urlParams.code, | ||
| 271 | error: urlParams.error, | ||
| 272 | errorDescription: urlParams.error_description, | ||
| 273 | }), | ||
| 274 | ); | ||
| 275 | |||
| 276 | if (Either.isRight(completed)) { | ||
| 277 | return htmlResponse( | ||
| 278 | sourceOAuthPopupResultDocument({ | ||
| 279 | title: "OAuth connected", | ||
| 280 | message: "OAuth credentials are ready. Return to the source form to finish saving.", | ||
| 281 | status: "connected", | ||
| 282 | sessionId: completed.right.sessionId, | ||
| 283 | payload: { | ||
| 284 | type: "executor:oauth-result", | ||
| 285 | ok: true, | ||
| 286 | sessionId: completed.right.sessionId, | ||
| 287 | auth: completed.right.auth, | ||
| 288 | }, | ||
| 289 | }), | ||
| 290 | ); | ||
| 291 | } | ||
| 292 | |||
| 293 | const message = completed.left instanceof Error | ||
| 294 | ? completed.left.message | ||
| 295 | : "Failed completing OAuth"; | ||
| 296 | |||
| 297 | return htmlResponse( | ||
| 298 | sourceOAuthPopupResultDocument({ | ||
| 299 | title: "OAuth failed", | ||
| 300 | message, | ||
| 301 | status: "failed", | ||
| 302 | sessionId: "failed", | ||
| 303 | payload: { | ||
| 304 | type: "executor:oauth-result", | ||
| 305 | ok: false, | ||
| 306 | sessionId: null, | ||
| 307 | error: message, | ||
| 308 | }, | ||
| 309 | }), | ||
| 310 | 500, | ||
| 311 | ); | ||
| 312 | }), | ||
| 313 | ), | ||
| 314 | ), | ||
| 315 | ); | ||
| @@ -1,13 +1,9 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | import { | 2 | import { |
| 3 | CreateSourcePayloadSchema, | 3 | CreateSourcePayloadSchema, |
| 4 | CreateScopeOauthClientPayloadSchema as CreateWorkspaceOauthClientPayloadSchema, | ||
| 5 | CredentialOauthCompleteUrlParamsSchema, | ||
| 6 | CredentialPageUrlParamsSchema, | 4 | CredentialPageUrlParamsSchema, |
| 7 | CredentialSubmitPayloadSchema, | 5 | CredentialSubmitPayloadSchema, |
| 8 | DiscoverSourcePayloadSchema, | ||
| 9 | UpdateSourcePayloadSchema, | 6 | UpdateSourcePayloadSchema, |
| 10 | ScopeOauthClientQuerySchema as WorkspaceOauthClientQuerySchema, | ||
| 11 | } from "@executor/platform-sdk/contracts"; | 7 | } from "@executor/platform-sdk/contracts"; |
| 12 | import { | 8 | import { |
| 13 | ControlPlaneBadRequestError, | 9 | ControlPlaneBadRequestError, |
| @@ -17,90 +13,35 @@ import { · 3 unmodified lines | |||
| 17 | ControlPlaneUnauthorizedError, | 13 | ControlPlaneUnauthorizedError, |
| 18 | } from "@executor/platform-sdk/errors"; | 14 | } from "@executor/platform-sdk/errors"; |
| 19 | import { | 15 | import { |
| 20 | ProviderAuthGrantIdSchema, | ||
| 21 | SourceDiscoveryResultSchema, | ||
| 22 | SourceIdSchema, | 16 | SourceIdSchema, |
| 23 | SourceInspectionDiscoverPayloadSchema, | 17 | SourceInspectionDiscoverPayloadSchema, |
| 24 | SourceInspectionDiscoverResultSchema, | 18 | SourceInspectionDiscoverResultSchema, |
| 25 | SourceInspectionSchema, | 19 | SourceInspectionSchema, |
| 26 | SourceInspectionToolDetailSchema, | 20 | SourceInspectionToolDetailSchema, |
| 27 | SourceSchema, | 21 | SourceSchema, |
| 28 | ScopeOauthClientIdSchema as WorkspaceOauthClientIdSchema, | ||
| 29 | ScopeOauthClientSchema as WorkspaceOauthClientSchema, | ||
| 30 | ScopeIdSchema as WorkspaceIdSchema, | 22 | ScopeIdSchema as WorkspaceIdSchema, |
| 31 | } from "@executor/platform-sdk/schema"; | 23 | } from "@executor/platform-sdk/schema"; |
| 32 | import * as Schema from "effect/Schema"; | 24 | import * as Schema from "effect/Schema"; |
| 33 | 25 | ||
| 34 | export type { | 26 | export type { |
| 35 | CreateSourcePayload, | 27 | CreateSourcePayload, |
| 36 | CreateScopeOauthClientPayload as CreateWorkspaceOauthClientPayload, | ||
| 37 | DiscoverSourcePayload, | ||
| 38 | UpdateSourcePayload, | 28 | UpdateSourcePayload, |
| 39 | } from "@executor/platform-sdk/contracts"; | 29 | } from "@executor/platform-sdk/contracts"; |
| 40 | 30 | ||
| 41 | export { | 31 | export { |
| 42 | CreateSourcePayloadSchema, | 32 | CreateSourcePayloadSchema, |
| 43 | CreateWorkspaceOauthClientPayloadSchema, | ||
| 44 | DiscoverSourcePayloadSchema, | ||
| 45 | UpdateSourcePayloadSchema, | 33 | UpdateSourcePayloadSchema, |
| 46 | }; | 34 | }; |
| 47 | 35 | ||
| 48 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | 36 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 49 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); | 37 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); |
| 50 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); | 38 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); |
| 51 | const oauthClientIdParam = HttpApiSchema.param( | ||
| 52 | "oauthClientId", | ||
| 53 | WorkspaceOauthClientIdSchema, | ||
| 54 | ); | ||
| 55 | const grantIdParam = HttpApiSchema.param("grantId", ProviderAuthGrantIdSchema); | ||
| 56 | 39 | ||
| 57 | const HtmlSchema = HttpApiSchema.Text({ | 40 | const HtmlSchema = HttpApiSchema.Text({ |
| 58 | contentType: "text/html", | 41 | contentType: "text/html", |
| 59 | }); | 42 | }); |
| 60 | 43 | ||
| 61 | export class SourcesApi extends HttpApiGroup.make("sources") | 44 | export class SourcesApi extends HttpApiGroup.make("sources") |
| 62 | .add( | ||
| 63 | HttpApiEndpoint.post("discover")`/sources/discover` | ||
| 64 | .setPayload(DiscoverSourcePayloadSchema) | ||
| 65 | .addSuccess(SourceDiscoveryResultSchema) | ||
| 66 | .addError(ControlPlaneBadRequestError) | ||
| 67 | .addError(ControlPlaneUnauthorizedError) | ||
| 68 | .addError(ControlPlaneForbiddenError), | ||
| 69 | ) | ||
| 70 | .add( | ||
| 71 | HttpApiEndpoint.get("listWorkspaceOauthClients")`/workspaces/${workspaceIdParam}/oauth-clients` | ||
| 72 | .setUrlParams(WorkspaceOauthClientQuerySchema) | ||
| 73 | .addSuccess(Schema.Array(WorkspaceOauthClientSchema)) | ||
| 74 | .addError(ControlPlaneBadRequestError) | ||
| 75 | .addError(ControlPlaneUnauthorizedError) | ||
| 76 | .addError(ControlPlaneForbiddenError) | ||
| 77 | .addError(ControlPlaneStorageError), | ||
| 78 | ) | ||
| 79 | .add( | ||
| 80 | HttpApiEndpoint.post("createWorkspaceOauthClient")`/workspaces/${workspaceIdParam}/oauth-clients` | ||
| 81 | .setPayload(CreateWorkspaceOauthClientPayloadSchema) | ||
| 82 | .addSuccess(WorkspaceOauthClientSchema) | ||
| 83 | .addError(ControlPlaneBadRequestError) | ||
| 84 | .addError(ControlPlaneUnauthorizedError) | ||
| 85 | .addError(ControlPlaneForbiddenError) | ||
| 86 | .addError(ControlPlaneStorageError), | ||
| 87 | ) | ||
| 88 | .add( | ||
| 89 | HttpApiEndpoint.del("removeWorkspaceOauthClient")`/workspaces/${workspaceIdParam}/oauth-clients/${oauthClientIdParam}` | ||
| 90 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | ||
| 91 | .addError(ControlPlaneBadRequestError) | ||
| 92 | .addError(ControlPlaneUnauthorizedError) | ||
| 93 | .addError(ControlPlaneForbiddenError) | ||
| 94 | .addError(ControlPlaneStorageError), | ||
| 95 | ) | ||
| 96 | .add( | ||
| 97 | HttpApiEndpoint.del("removeProviderAuthGrant")`/workspaces/${workspaceIdParam}/provider-grants/${grantIdParam}` | ||
| 98 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | ||
| 99 | .addError(ControlPlaneBadRequestError) | ||
| 100 | .addError(ControlPlaneUnauthorizedError) | ||
| 101 | .addError(ControlPlaneForbiddenError) | ||
| 102 | .addError(ControlPlaneStorageError), | ||
| 103 | ) | ||
| 104 | .add( | 45 | .add( |
| 105 | HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources` | 46 | HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources` |
| 106 | .addSuccess(Schema.Array(SourceSchema)) | 47 | .addSuccess(Schema.Array(SourceSchema)) |
| @@ -162,22 +103,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 55 unmodified lines | |||
| 162 | .addError(ControlPlaneNotFoundError) | 103 | .addError(ControlPlaneNotFoundError) |
| 163 | .addError(ControlPlaneStorageError), | 104 | .addError(ControlPlaneStorageError), |
| 164 | ) | 105 | ) |
| 165 | .add( | ||
| 166 | HttpApiEndpoint.get("credentialComplete")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/credentials/oauth/complete` | ||
| 167 | .setUrlParams(CredentialOauthCompleteUrlParamsSchema) | ||
| 168 | .addSuccess(HtmlSchema) | ||
| 169 | .addError(ControlPlaneBadRequestError) | ||
| 170 | .addError(ControlPlaneNotFoundError) | ||
| 171 | .addError(ControlPlaneStorageError), | ||
| 172 | ) | ||
| 173 | .add( | ||
| 174 | HttpApiEndpoint.get("providerOauthComplete")`/workspaces/${workspaceIdParam}/oauth/provider/callback` | ||
| 175 | .setUrlParams(CredentialOauthCompleteUrlParamsSchema) | ||
| 176 | .addSuccess(HtmlSchema) | ||
| 177 | .addError(ControlPlaneBadRequestError) | ||
| 178 | .addError(ControlPlaneNotFoundError) | ||
| 179 | .addError(ControlPlaneStorageError), | ||
| 180 | ) | ||
| 181 | .add( | 106 | .add( |
| 182 | HttpApiEndpoint.get("inspection")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection` | 107 | HttpApiEndpoint.get("inspection")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection` |
| 183 | .addSuccess(SourceInspectionSchema) | 108 | .addSuccess(SourceInspectionSchema) |
| @@ -37,14 +37,6 @@ const escapeHtml = (value: string): string => · 36 unmodified lines | |||
| 37 | .replaceAll('"', """) | 37 | .replaceAll('"', """) |
| 38 | .replaceAll("'", "'"); | 38 | .replaceAll("'", "'"); |
| 39 | 39 | ||
| 40 | const serializeJsonForScript = (value: unknown): string => | ||
| 41 | JSON.stringify(value) | ||
| 42 | .replaceAll("<", "\\u003c") | ||
| 43 | .replaceAll(">", "\\u003e") | ||
| 44 | .replaceAll("&", "\\u0026"); | ||
| 45 | |||
| 46 | const SOURCE_OAUTH_RESULT_STORAGE_KEY_PREFIX = "executor:oauth-result:"; | ||
| 47 | |||
| 48 | const credentialPageDocument = (input: { | 40 | const credentialPageDocument = (input: { |
| 49 | title: string; | 41 | title: string; |
| 50 | eyebrow: string; | 42 | eyebrow: string; |
| @@ -404,133 +396,6 @@ const credentialPageDocument = (input: { · 353 unmodified lines | |||
| 404 | </body> | 396 | </body> |
| 405 | </html>`; | 397 | </html>`; |
| 406 | 398 | ||
| 407 | const sourceOAuthPopupResultDocument = (input: { | ||
| 408 | title: string; | ||
| 409 | message: string; | ||
| 410 | state: "stored" | "cancelled"; | ||
| 411 | sessionId?: string | null; | ||
| 412 | payload: | ||
| 413 | | { | ||
| 414 | type: "executor:source-oauth-result"; | ||
| 415 | ok: true; | ||
| 416 | sourceId: Source["id"]; | ||
| 417 | } | ||
| 418 | | { | ||
| 419 | type: "executor:source-oauth-result"; | ||
| 420 | ok: false; | ||
| 421 | error: string; | ||
| 422 | }; | ||
| 423 | }) => `<!doctype html> | ||
| 424 | <html lang="en"> | ||
| 425 | <head> | ||
| 426 | <meta charset="utf-8" /> | ||
| 427 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 428 | <title>${escapeHtml(input.title)}</title> | ||
| 429 | <style> | ||
| 430 | :root { | ||
| 431 | --bg: #fafaf9; | ||
| 432 | --surface: #ffffff; | ||
| 433 | --text: #1c1917; | ||
| 434 | --text-secondary: #78716c; | ||
| 435 | --border: #e7e5e4; | ||
| 436 | --green: #16a34a; | ||
| 437 | --green-light: #f0fdf4; | ||
| 438 | --green-border: #bbf7d0; | ||
| 439 | --red: #dc2626; | ||
| 440 | --red-light: #fef2f2; | ||
| 441 | --red-border: #fecaca; | ||
| 442 | } | ||
| 443 | |||
| 444 | * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| 445 | |||
| 446 | body { | ||
| 447 | min-height: 100vh; | ||
| 448 | display: flex; | ||
| 449 | align-items: center; | ||
| 450 | justify-content: center; | ||
| 451 | background: var(--bg); | ||
| 452 | color: var(--text); | ||
| 453 | font-family: system-ui, -apple-system, sans-serif; | ||
| 454 | padding: 24px; | ||
| 455 | } | ||
| 456 | |||
| 457 | .card { | ||
| 458 | width: 100%; | ||
| 459 | max-width: 420px; | ||
| 460 | background: var(--surface); | ||
| 461 | border: 1px solid var(--border); | ||
| 462 | border-radius: 16px; | ||
| 463 | padding: 28px; | ||
| 464 | text-align: center; | ||
| 465 | } | ||
| 466 | |||
| 467 | .status { | ||
| 468 | display: inline-flex; | ||
| 469 | align-items: center; | ||
| 470 | justify-content: center; | ||
| 471 | padding: 6px 12px; | ||
| 472 | border-radius: 999px; | ||
| 473 | font-size: 12px; | ||
| 474 | font-weight: 600; | ||
| 475 | margin-bottom: 14px; | ||
| 476 | } | ||
| 477 | |||
| 478 | .status[data-state='stored'] { | ||
| 479 | background: var(--green-light); | ||
| 480 | border: 1px solid var(--green-border); | ||
| 481 | color: var(--green); | ||
| 482 | } | ||
| 483 | |||
| 484 | .status[data-state='cancelled'] { | ||
| 485 | background: var(--red-light); | ||
| 486 | border: 1px solid var(--red-border); | ||
| 487 | color: var(--red); | ||
| 488 | } | ||
| 489 | |||
| 490 | h1 { | ||
| 491 | font-size: 22px; | ||
| 492 | line-height: 1.2; | ||
| 493 | margin-bottom: 10px; | ||
| 494 | } | ||
| 495 | |||
| 496 | p { | ||
| 497 | color: var(--text-secondary); | ||
| 498 | font-size: 14px; | ||
| 499 | line-height: 1.6; | ||
| 500 | } | ||
| 501 | </style> | ||
| 502 | </head> | ||
| 503 | <body> | ||
| 504 | <main class="card"> | ||
| 505 | <div class="status" data-state="${escapeHtml(input.state)}">${escapeHtml(input.state === "stored" ? "connected" : "failed")}</div> | ||
| 506 | <h1>${escapeHtml(input.title)}</h1> | ||
| 507 | <p>${escapeHtml(input.message)}</p> | ||
| 508 | </main> | ||
| 509 | <script> | ||
| 510 | (() => { | ||
| 511 | const payload = ${serializeJsonForScript(input.payload)}; | ||
| 512 | const storageKey = ${serializeJsonForScript( | ||
| 513 | input.sessionId | ||
| 514 | ? `${SOURCE_OAUTH_RESULT_STORAGE_KEY_PREFIX}${input.sessionId}` | ||
| 515 | : null, | ||
| 516 | )}; | ||
| 517 | try { | ||
| 518 | if (storageKey) { | ||
| 519 | window.localStorage.setItem(storageKey, JSON.stringify(payload)); | ||
| 520 | } | ||
| 521 | } catch {} | ||
| 522 | try { | ||
| 523 | if (window.opener) { | ||
| 524 | window.opener.postMessage(payload, window.location.origin); | ||
| 525 | } | ||
| 526 | } finally { | ||
| 527 | window.setTimeout(() => window.close(), 120); | ||
| 528 | } | ||
| 529 | })(); | ||
| 530 | </script> | ||
| 531 | </body> | ||
| 532 | </html>`; | ||
| 533 | |||
| 534 | const htmlResponse = ( | 399 | const htmlResponse = ( |
| 535 | html: string, | 400 | html: string, |
| 536 | status = 200, | 401 | status = 200, |
| @@ -616,93 +481,6 @@ export const ExecutorSourcesLive = HttpApiBuilder.group( · 79 unmodified lines | |||
| 616 | "sources", | 481 | "sources", |
| 617 | (handlers) => | 482 | (handlers) => |
| 618 | handlers | 483 | handlers |
| 619 | .handle("discover", ({ payload }) => | ||
| 620 | Effect.flatMap(getControlPlaneExecutor(), (executor) => | ||
| 621 | executor.sources.discover({ | ||
| 622 | url: payload.url, | ||
| 623 | probeAuth: payload.probeAuth, | ||
| 624 | }).pipe( | ||
| 625 | Effect.catchAll((cause) => | ||
| 626 | Effect.fail(toBadRequestError("sources.discover", cause)), | ||
| 627 | ), | ||
| 628 | ) | ||
| 629 | ) | ||
| 630 | ) | ||
| 631 | .handle("listWorkspaceOauthClients", ({ path, urlParams }) => | ||
| 632 | resolveRequestedLocalWorkspace( | ||
| 633 | "sources.listWorkspaceOauthClients", | ||
| 634 | path.workspaceId, | ||
| 635 | ).pipe( | ||
| 636 | Effect.flatMap((executor) => | ||
| 637 | executor.sources.oauthClients.list(urlParams.providerKey).pipe( | ||
| 638 | Effect.catchAll((cause) => | ||
| 639 | Effect.fail( | ||
| 640 | toBadRequestError("sources.listWorkspaceOauthClients", cause), | ||
| 641 | ), | ||
| 642 | ), | ||
| 643 | ) | ||
| 644 | ), | ||
| 645 | ), | ||
| 646 | ) | ||
| 647 | .handle("createWorkspaceOauthClient", ({ path, payload }) => | ||
| 648 | resolveRequestedLocalWorkspace( | ||
| 649 | "sources.createWorkspaceOauthClient", | ||
| 650 | path.workspaceId, | ||
| 651 | ).pipe( | ||
| 652 | Effect.flatMap((executor) => | ||
| 653 | executor.sources.oauthClients.create(payload).pipe( | ||
| 654 | Effect.catchAll((cause) => | ||
| 655 | Effect.fail( | ||
| 656 | toBadRequestError( | ||
| 657 | "sources.createWorkspaceOauthClient", | ||
| 658 | cause, | ||
| 659 | ), | ||
| 660 | ), | ||
| 661 | ), | ||
| 662 | ) | ||
| 663 | ), | ||
| 664 | ), | ||
| 665 | ) | ||
| 666 | .handle("removeWorkspaceOauthClient", ({ path }) => | ||
| 667 | resolveRequestedLocalWorkspace( | ||
| 668 | "sources.removeWorkspaceOauthClient", | ||
| 669 | path.workspaceId, | ||
| 670 | ).pipe( | ||
| 671 | Effect.flatMap((executor) => | ||
| 672 | executor.sources.oauthClients.remove(path.oauthClientId).pipe( | ||
| 673 | Effect.map((result) => ({ removed: result })), | ||
| 674 | Effect.catchAll((cause) => | ||
| 675 | Effect.fail( | ||
| 676 | toBadRequestError( | ||
| 677 | "sources.removeWorkspaceOauthClient", | ||
| 678 | cause, | ||
| 679 | ), | ||
| 680 | ), | ||
| 681 | ), | ||
| 682 | ) | ||
| 683 | ), | ||
| 684 | ), | ||
| 685 | ) | ||
| 686 | .handle("removeProviderAuthGrant", ({ path }) => | ||
| 687 | resolveRequestedLocalWorkspace( | ||
| 688 | "sources.removeProviderAuthGrant", | ||
| 689 | path.workspaceId, | ||
| 690 | ).pipe( | ||
| 691 | Effect.flatMap((executor) => | ||
| 692 | executor.sources.providerGrants.remove(path.grantId).pipe( | ||
| 693 | Effect.map((result) => ({ removed: result })), | ||
| 694 | Effect.catchAll((cause) => | ||
| 695 | Effect.fail( | ||
| 696 | toBadRequestError( | ||
| 697 | "sources.removeProviderAuthGrant", | ||
| 698 | cause, | ||
| 699 | ), | ||
| 700 | ), | ||
| 701 | ), | ||
| 702 | ) | ||
| 703 | ), | ||
| 704 | ), | ||
| 705 | ) | ||
| 706 | .handle("list", ({ path }) => | 484 | .handle("list", ({ path }) => |
| 707 | resolveRequestedLocalWorkspace("sources.list", path.workspaceId).pipe( | 485 | resolveRequestedLocalWorkspace("sources.list", path.workspaceId).pipe( |
| 708 | Effect.flatMap((executor) => executor.sources.list()), | 486 | Effect.flatMap((executor) => executor.sources.list()), |
| @@ -883,140 +661,4 @@ export const ExecutorSourcesLive = HttpApiBuilder.group( · 174 unmodified lines | |||
| 883 | }), | 661 | }), |
| 884 | ), | 662 | ), |
| 885 | ) | 663 | ) |
| 886 | .handle("credentialComplete", ({ path, urlParams }) => | ||
| 887 | resolveRequestedLocalWorkspace( | ||
| 888 | "sources.credentialComplete", | ||
| 889 | path.workspaceId, | ||
| 890 | ).pipe( | ||
| 891 | Effect.flatMap((executor) => | ||
| 892 | executor.local.credentials.complete({ | ||
| 893 | sourceId: path.sourceId, | ||
| 894 | state: urlParams.state, | ||
| 895 | code: urlParams.code, | ||
| 896 | error: urlParams.error, | ||
| 897 | errorDescription: urlParams.error_description, | ||
| 898 | }), | ||
| 899 | ), | ||
| 900 | Effect.map((completed) => | ||
| 901 | htmlResponse( | ||
| 902 | sourceOAuthPopupResultDocument({ | ||
| 903 | title: "Source connected", | ||
| 904 | message: `Source connected: ${completed.source.id}. You can close this window.`, | ||
| 905 | state: "stored", | ||
| 906 | sessionId: completed.sessionId, | ||
| 907 | payload: { | ||
| 908 | type: "executor:source-oauth-result", | ||
| 909 | ok: true, | ||
| 910 | sourceId: completed.source.id, | ||
| 911 | }, | ||
| 912 | }), | ||
| 913 | ), | ||
| 914 | ), | ||
| 915 | Effect.catchAll((error) => | ||
| 916 | Effect.succeed( | ||
| 917 | htmlResponse( | ||
| 918 | sourceOAuthPopupResultDocument({ | ||
| 919 | title: "OAuth failed", | ||
| 920 | message: | ||
| 921 | error instanceof Error | ||
| 922 | ? error.message | ||
| 923 | : "Failed completing OAuth", | ||
| 924 | state: "cancelled", | ||
| 925 | payload: { | ||
| 926 | type: "executor:source-oauth-result", | ||
| 927 | ok: false, | ||
| 928 | error: | ||
| 929 | error instanceof Error | ||
| 930 | ? error.message | ||
| 931 | : "Failed completing OAuth", | ||
| 932 | }, | ||
| 933 | }), | ||
| 934 | 500, | ||
| 935 | ), | ||
| 936 | ), | ||
| 937 | ), | ||
| 938 | ), | ||
| 939 | ) | ||
| 940 | .handle("providerOauthComplete", ({ path, urlParams }) => | ||
| 941 | resolveRequestedLocalWorkspace( | ||
| 942 | "sources.providerOauthComplete", | ||
| 943 | path.workspaceId, | ||
| 944 | ).pipe( | ||
| 945 | Effect.flatMap((executor) => | ||
| 946 | Effect.gen(function* () { | ||
| 947 | const completed = yield* executor.oauth.completeProviderCallback({ | ||
| 948 | scopeId: path.workspaceId, | ||
| 949 | state: urlParams.state, | ||
| 950 | code: urlParams.code, | ||
| 951 | error: urlParams.error, | ||
| 952 | errorDescription: urlParams.error_description, | ||
| 953 | }); | ||
| 954 | const primarySource = completed.sources[0] ?? null; | ||
| 955 | |||
| 956 | return htmlResponse( | ||
| 957 | sourceOAuthPopupResultDocument({ | ||
| 958 | title: "Sources connected", | ||
| 959 | message: `Connected ${completed.sources.length} source${completed.sources.length === 1 ? "" : "s"}. You can close this window.`, | ||
| 960 | state: "stored", | ||
| 961 | sessionId: completed.sessionId, | ||
| 962 | payload: { | ||
| 963 | type: "executor:source-oauth-result", | ||
| 964 | ok: true, | ||
| 965 | sourceId: | ||
| 966 | primarySource?.id ?? | ||
| 967 | ("src_oauth_complete" as Source["id"]), | ||
| 968 | }, | ||
| 969 | }), | ||
| 970 | ); | ||
| 971 | }).pipe( | ||
| 972 | Effect.catchAll((error) => | ||
| 973 | Effect.succeed( | ||
| 974 | htmlResponse( | ||
| 975 | sourceOAuthPopupResultDocument({ | ||
| 976 | title: "OAuth failed", | ||
| 977 | message: | ||
| 978 | error instanceof Error | ||
| 979 | ? error.message | ||
| 980 | : "Failed completing OAuth", | ||
| 981 | state: "cancelled", | ||
| 982 | payload: { | ||
| 983 | type: "executor:source-oauth-result", | ||
| 984 | ok: false, | ||
| 985 | error: | ||
| 986 | error instanceof Error | ||
| 987 | ? error.message | ||
| 988 | : "Failed completing OAuth", | ||
| 989 | }, | ||
| 990 | }), | ||
| 991 | 500, | ||
| 992 | ), | ||
| 993 | ), | ||
| 994 | ), | ||
| 995 | ), | ||
| 996 | ), | ||
| 997 | Effect.catchAll((error) => | ||
| 998 | Effect.succeed( | ||
| 999 | htmlResponse( | ||
| 1000 | sourceOAuthPopupResultDocument({ | ||
| 1001 | title: "OAuth failed", | ||
| 1002 | message: | ||
| 1003 | error instanceof Error | ||
| 1004 | ? error.message | ||
| 1005 | : "Failed completing OAuth", | ||
| 1006 | state: "cancelled", | ||
| 1007 | payload: { | ||
| 1008 | type: "executor:source-oauth-result", | ||
| 1009 | ok: false, | ||
| 1010 | error: | ||
| 1011 | error instanceof Error | ||
| 1012 | ? error.message | ||
| 1013 | : "Failed completing OAuth", | ||
| 1014 | }, | ||
| 1015 | }), | ||
| 1016 | 500, | ||
| 1017 | ), | ||
| 1018 | ), | ||
| 1019 | ), | ||
| 1020 | ), | ||
| 1021 | ), | ||
| 1022 | ); | 664 | ); |
| @@ -4,7 +4,6 @@ import { · 3 unmodified lines | |||
| 4 | type ScopeId as AccountId, | 4 | type ScopeId as AccountId, |
| 5 | LocalInstallationSchema, | 5 | LocalInstallationSchema, |
| 6 | LocalScopePolicySchema, | 6 | LocalScopePolicySchema, |
| 7 | SourceDiscoveryResultSchema, | ||
| 8 | SourceIdSchema, | 7 | SourceIdSchema, |
| 9 | SourceInspectionDiscoverPayloadSchema, | 8 | SourceInspectionDiscoverPayloadSchema, |
| 10 | SourceInspectionDiscoverResultSchema, | 9 | SourceInspectionDiscoverResultSchema, |
| @@ -12,7 +11,6 @@ import { · 1 unmodified line | |||
| 12 | SourceInspectionToolDetailSchema, | 11 | SourceInspectionToolDetailSchema, |
| 13 | SourceSchema, | 12 | SourceSchema, |
| 14 | type ScopeId as WorkspaceId, | 13 | type ScopeId as WorkspaceId, |
| 15 | ScopeOauthClientSchema, | ||
| 16 | } from "@executor/platform-sdk/schema"; | 14 | } from "@executor/platform-sdk/schema"; |
| 17 | import * as Effect from "effect/Effect"; | 15 | import * as Effect from "effect/Effect"; |
| 18 | import * as Layer from "effect/Layer"; | 16 | import * as Layer from "effect/Layer"; |
| @@ -59,7 +57,6 @@ import { · 40 unmodified lines | |||
| 59 | createLocalSecret, | 57 | createLocalSecret, |
| 60 | createPolicy, | 58 | createPolicy, |
| 61 | deleteLocalSecret, | 59 | deleteLocalSecret, |
| 62 | discoverSource, | ||
| 63 | discoverSourceInspectionTools, | 60 | discoverSourceInspectionTools, |
| 64 | getPolicy, | 61 | getPolicy, |
| 65 | getSource, | 62 | getSource, |
| @@ -81,9 +78,6 @@ import { · 15 unmodified lines | |||
| 81 | UpdatePolicyPayloadSchema, | 78 | UpdatePolicyPayloadSchema, |
| 82 | } from "@executor/platform-sdk/contracts"; | 79 | } from "@executor/platform-sdk/contracts"; |
| 83 | import { | 80 | import { |
| 84 | CreateScopeOauthClientPayloadSchema, | ||
| 85 | DiscoverSourcePayloadSchema, | ||
| 86 | type CreateScopeOauthClientPayload as CreateWorkspaceOauthClientPayload, | ||
| 87 | UpdateSourcePayloadSchema, | 81 | UpdateSourcePayloadSchema, |
| 88 | } from "@executor/platform-sdk/contracts"; | 82 | } from "@executor/platform-sdk/contracts"; |
| 89 | 83 | ||
| @@ -129,9 +123,6 @@ const updateSourceInputSchema = Schema.standardSchemaV1( · 39 unmodified lines | |||
| 129 | payload: UpdateSourcePayloadSchema, | 123 | payload: UpdateSourcePayloadSchema, |
| 130 | }), | 124 | }), |
| 131 | ); | 125 | ); |
| 132 | const discoverSourceInputSchema = Schema.standardSchemaV1( | ||
| 133 | DiscoverSourcePayloadSchema, | ||
| 134 | ); | ||
| 135 | const inspectToolInputSchema = Schema.standardSchemaV1( | 126 | const inspectToolInputSchema = Schema.standardSchemaV1( |
| 136 | Schema.Struct({ | 127 | Schema.Struct({ |
| 137 | sourceId: SourceIdSchema, | 128 | sourceId: SourceIdSchema, |
| @@ -161,27 +152,6 @@ const updatePolicyInputSchema = Schema.standardSchemaV1( · 23 unmodified lines | |||
| 161 | payload: UpdatePolicyPayloadSchema, | 152 | payload: UpdatePolicyPayloadSchema, |
| 162 | }), | 153 | }), |
| 163 | ); | 154 | ); |
| 164 | const workspaceOauthClientListInputSchema = Schema.standardSchemaV1( | ||
| 165 | Schema.Struct({ | ||
| 166 | providerKey: Schema.String, | ||
| 167 | }), | ||
| 168 | ); | ||
| 169 | const workspaceOauthClientListOutputSchema = Schema.standardSchemaV1( | ||
| 170 | Schema.Array(ScopeOauthClientSchema), | ||
| 171 | ); | ||
| 172 | const createWorkspaceOauthClientInputSchema = Schema.standardSchemaV1( | ||
| 173 | CreateScopeOauthClientPayloadSchema, | ||
| 174 | ); | ||
| 175 | const removeWorkspaceOauthClientInputSchema = Schema.standardSchemaV1( | ||
| 176 | Schema.Struct({ | ||
| 177 | oauthClientId: Schema.String, | ||
| 178 | }), | ||
| 179 | ); | ||
| 180 | const removeProviderGrantInputSchema = Schema.standardSchemaV1( | ||
| 181 | Schema.Struct({ | ||
| 182 | grantId: Schema.String, | ||
| 183 | }), | ||
| 184 | ); | ||
| 185 | const sourceOutputSchema = Schema.standardSchemaV1(SourceSchema); | 155 | const sourceOutputSchema = Schema.standardSchemaV1(SourceSchema); |
| 186 | const sourceInspectionOutputSchema = Schema.standardSchemaV1( | 156 | const sourceInspectionOutputSchema = Schema.standardSchemaV1( |
| 187 | SourceInspectionSchema, | 157 | SourceInspectionSchema, |
| @@ -192,9 +162,6 @@ const sourceInspectionToolOutputSchema = Schema.standardSchemaV1( · 4 unmodified lines | |||
| 192 | const sourceInspectionDiscoverOutputSchema = Schema.standardSchemaV1( | 162 | const sourceInspectionDiscoverOutputSchema = Schema.standardSchemaV1( |
| 193 | SourceInspectionDiscoverResultSchema, | 163 | SourceInspectionDiscoverResultSchema, |
| 194 | ); | 164 | ); |
| 195 | const sourceDiscoveryOutputSchema = Schema.standardSchemaV1( | ||
| 196 | SourceDiscoveryResultSchema, | ||
| 197 | ); | ||
| 198 | const localScopePolicyOutputSchema = Schema.standardSchemaV1( | 165 | const localScopePolicyOutputSchema = Schema.standardSchemaV1( |
| 199 | LocalScopePolicySchema, | 166 | LocalScopePolicySchema, |
| 200 | ); | 167 | ); |
| @@ -384,22 +351,6 @@ export const createWorkspaceExecutorAdminToolMap = ( · 183 unmodified lines | |||
| 384 | }, | 351 | }, |
| 385 | metadata, | 352 | metadata, |
| 386 | }), | 353 | }), |
| 387 | "executor.sources.discover": toTool({ | ||
| 388 | tool: { | ||
| 389 | description: | ||
| 390 | "Probe a URL and infer whether it looks like MCP, OpenAPI, GraphQL, or another supported source.", | ||
| 391 | inputSchema: discoverSourceInputSchema, | ||
| 392 | outputSchema: sourceDiscoveryOutputSchema, | ||
| 393 | execute: (payload: { url: string; probeAuth?: unknown }) => | ||
| 394 | Effect.runPromise( | ||
| 395 | discoverSource({ | ||
| 396 | url: payload.url, | ||
| 397 | probeAuth: payload.probeAuth as never, | ||
| 398 | }), | ||
| 399 | ), | ||
| 400 | }, | ||
| 401 | metadata, | ||
| 402 | }), | ||
| 403 | "executor.sources.list": toTool({ | 354 | "executor.sources.list": toTool({ |
| 404 | tool: { | 355 | tool: { |
| 405 | description: "List sources connected in the current workspace.", | 356 | description: "List sources connected in the current workspace.", |
| @@ -539,82 +490,6 @@ export const createWorkspaceExecutorAdminToolMap = ( · 133 unmodified lines | |||
| 539 | }, | 490 | }, |
| 540 | metadata, | 491 | metadata, |
| 541 | }), | 492 | }), |
| 542 | "executor.sources.oauthClients.list": toTool({ | ||
| 543 | tool: { | ||
| 544 | description: "List workspace OAuth clients for a provider key.", | ||
| 545 | inputSchema: workspaceOauthClientListInputSchema, | ||
| 546 | outputSchema: workspaceOauthClientListOutputSchema, | ||
| 547 | execute: ({ providerKey }: { providerKey: string }) => | ||
| 548 | runScopeStorageEffect({ | ||
| 549 | effect: input.sourceAuthService.listScopeOauthClients({ | ||
| 550 | scopeId: input.scopeId, | ||
| 551 | providerKey, | ||
| 552 | }), | ||
| 553 | scopeStorageLayer, | ||
| 554 | runtimeLocalScope: input.runtimeLocalScope, | ||
| 555 | }), | ||
| 556 | }, | ||
| 557 | metadata, | ||
| 558 | }), | ||
| 559 | "executor.sources.oauthClients.create": toTool({ | ||
| 560 | tool: { | ||
| 561 | description: | ||
| 562 | "Create a workspace OAuth client used for shared provider auth flows.", | ||
| 563 | inputSchema: createWorkspaceOauthClientInputSchema, | ||
| 564 | outputSchema: Schema.standardSchemaV1(ScopeOauthClientSchema), | ||
| 565 | execute: (payload: CreateWorkspaceOauthClientPayload) => | ||
| 566 | runScopeStorageEffect({ | ||
| 567 | effect: input.sourceAuthService.createScopeOauthClient({ | ||
| 568 | scopeId: input.scopeId, | ||
| 569 | providerKey: payload.providerKey, | ||
| 570 | label: payload.label, | ||
| 571 | oauthClient: payload.oauthClient, | ||
| 572 | }), | ||
| 573 | scopeStorageLayer, | ||
| 574 | runtimeLocalScope: input.runtimeLocalScope, | ||
| 575 | }), | ||
| 576 | }, | ||
| 577 | metadata, | ||
| 578 | }), | ||
| 579 | "executor.sources.oauthClients.remove": toTool({ | ||
| 580 | tool: { | ||
| 581 | description: "Remove a workspace OAuth client.", | ||
| 582 | inputSchema: removeWorkspaceOauthClientInputSchema, | ||
| 583 | outputSchema: removeResultSchema, | ||
| 584 | execute: ({ oauthClientId }: { oauthClientId: string }) => | ||
| 585 | runScopeStorageEffect({ | ||
| 586 | effect: input.sourceAuthService | ||
| 587 | .removeScopeOauthClient({ | ||
| 588 | scopeId: input.scopeId, | ||
| 589 | oauthClientId: oauthClientId as never, | ||
| 590 | }) | ||
| 591 | .pipe(Effect.map((removed) => ({ removed }))), | ||
| 592 | scopeStorageLayer, | ||
| 593 | runtimeLocalScope: input.runtimeLocalScope, | ||
| 594 | }), | ||
| 595 | }, | ||
| 596 | metadata, | ||
| 597 | }), | ||
| 598 | "executor.sources.providerGrants.remove": toTool({ | ||
| 599 | tool: { | ||
| 600 | description: | ||
| 601 | "Revoke a shared provider grant reference from the local workspace.", | ||
| 602 | inputSchema: removeProviderGrantInputSchema, | ||
| 603 | outputSchema: removeResultSchema, | ||
| 604 | execute: ({ grantId }: { grantId: string }) => | ||
| 605 | runScopeStorageEffect({ | ||
| 606 | effect: input.sourceAuthService | ||
| 607 | .removeProviderAuthGrant({ | ||
| 608 | scopeId: input.scopeId, | ||
| 609 | grantId: grantId as never, | ||
| 610 | }) | ||
| 611 | .pipe(Effect.map((removed) => ({ removed }))), | ||
| 612 | scopeStorageLayer, | ||
| 613 | runtimeLocalScope: input.runtimeLocalScope, | ||
| 614 | }), | ||
| 615 | }, | ||
| 616 | metadata, | ||
| 617 | }), | ||
| 618 | "executor.policies.list": toTool({ | 493 | "executor.policies.list": toTool({ |
| 619 | tool: { | 494 | tool: { |
| 620 | description: "List local workspace policies.", | 495 | description: "List local workspace policies.", |
| @@ -780,20 +655,6 @@ export const createExecutorAdminToolMap = (input: { · 159 unmodified lines | |||
| 780 | }, | 655 | }, |
| 781 | metadata, | 656 | metadata, |
| 782 | }), | 657 | }), |
| 783 | "executor.sources.discover": toTool({ | ||
| 784 | tool: { | ||
| 785 | description: | ||
| 786 | "Probe a URL and infer whether it looks like MCP, OpenAPI, GraphQL, or another supported source.", | ||
| 787 | inputSchema: discoverSourceInputSchema, | ||
| 788 | outputSchema: sourceDiscoveryOutputSchema, | ||
| 789 | execute: (payload: { url: string; probeAuth?: unknown }) => | ||
| 790 | input.executor.sources.discover({ | ||
| 791 | url: payload.url, | ||
| 792 | probeAuth: payload.probeAuth as never, | ||
| 793 | }), | ||
| 794 | }, | ||
| 795 | metadata, | ||
| 796 | }), | ||
| 797 | "executor.sources.list": toTool({ | 658 | "executor.sources.list": toTool({ |
| 798 | tool: { | 659 | tool: { |
| 799 | description: "List sources connected in the current workspace.", | 660 | description: "List sources connected in the current workspace.", |
| @@ -888,57 +749,6 @@ export const createExecutorAdminToolMap = (input: { · 88 unmodified lines | |||
| 888 | }, | 749 | }, |
| 889 | metadata, | 750 | metadata, |
| 890 | }), | 751 | }), |
| 891 | "executor.sources.oauthClients.list": toTool({ | ||
| 892 | tool: { | ||
| 893 | description: "List stored workspace OAuth clients for one provider.", | ||
| 894 | inputSchema: workspaceOauthClientListInputSchema, | ||
| 895 | outputSchema: workspaceOauthClientListOutputSchema, | ||
| 896 | execute: ({ providerKey }: { providerKey: string }) => | ||
| 897 | input.executor.sources.oauthClients.list(providerKey), | ||
| 898 | }, | ||
| 899 | metadata, | ||
| 900 | }), | ||
| 901 | "executor.sources.oauthClients.create": toTool({ | ||
| 902 | tool: { | ||
| 903 | description: | ||
| 904 | "Create a workspace OAuth client for a provider-backed source flow.", | ||
| 905 | inputSchema: createWorkspaceOauthClientInputSchema, | ||
| 906 | outputSchema: Schema.standardSchemaV1(ScopeOauthClientSchema), | ||
| 907 | execute: (payload: CreateWorkspaceOauthClientPayload) => | ||
| 908 | input.executor.sources.oauthClients.create(payload), | ||
| 909 | }, | ||
| 910 | metadata, | ||
| 911 | }), | ||
| 912 | "executor.sources.oauthClients.remove": toTool({ | ||
| 913 | tool: { | ||
| 914 | description: "Remove a stored workspace OAuth client.", | ||
| 915 | inputSchema: removeWorkspaceOauthClientInputSchema, | ||
| 916 | outputSchema: removeResultSchema, | ||
| 917 | execute: async ({ | ||
| 918 | oauthClientId, | ||
| 919 | }: { | ||
| 920 | oauthClientId: string; | ||
| 921 | }) => ({ | ||
| 922 | removed: await input.executor.sources.oauthClients.remove( | ||
| 923 | oauthClientId as never, | ||
| 924 | ), | ||
| 925 | }), | ||
| 926 | }, | ||
| 927 | metadata, | ||
| 928 | }), | ||
| 929 | "executor.sources.providerGrants.remove": toTool({ | ||
| 930 | tool: { | ||
| 931 | description: "Remove one stored provider auth grant.", | ||
| 932 | inputSchema: removeProviderGrantInputSchema, | ||
| 933 | outputSchema: removeResultSchema, | ||
| 934 | execute: async ({ grantId }: { grantId: string }) => ({ | ||
| 935 | removed: await input.executor.sources.providerGrants.remove( | ||
| 936 | grantId as never, | ||
| 937 | ), | ||
| 938 | }), | ||
| 939 | }, | ||
| 940 | metadata, | ||
| 941 | }), | ||
| 942 | "executor.policies.list": toTool({ | 752 | "executor.policies.list": toTool({ |
| 943 | tool: { | 753 | tool: { |
| 944 | description: "List local workspace policies.", | 754 | description: "List local workspace policies.", |
| @@ -7,8 +7,6 @@ import type { · 6 unmodified lines | |||
| 7 | ExecutionInteraction, | 7 | ExecutionInteraction, |
| 8 | LocalInstallation, | 8 | LocalInstallation, |
| 9 | LocalScopePolicy, | 9 | LocalScopePolicy, |
| 10 | ProviderAuthGrant, | ||
| 11 | ScopeOauthClient, | ||
| 12 | Source, | 10 | Source, |
| 13 | } from "./schema"; | 11 | } from "./schema"; |
| 14 | import { ExecutionIdSchema } from "./schema"; | 12 | import { ExecutionIdSchema } from "./schema"; |
| @@ -50,11 +48,9 @@ import { · 35 unmodified lines | |||
| 50 | updatePolicy, | 48 | updatePolicy, |
| 51 | } from "./policies/operations"; | 49 | } from "./policies/operations"; |
| 52 | import type { | 50 | import type { |
| 53 | CreateScopeOauthClientPayload, | ||
| 54 | CreateSourcePayload, | 51 | CreateSourcePayload, |
| 55 | UpdateSourcePayload, | 52 | UpdateSourcePayload, |
| 56 | } from "./sources/contracts"; | 53 | } from "./sources/contracts"; |
| 57 | import { discoverSource } from "./sources/discovery"; | ||
| 58 | import { | 54 | import { |
| 59 | discoverSourceInspectionTools, | 55 | discoverSourceInspectionTools, |
| 60 | getSourceInspection, | 56 | getSourceInspection, |
| @@ -83,13 +79,9 @@ import { · 22 unmodified lines | |||
| 83 | resumeExecution, | 79 | resumeExecution, |
| 84 | } from "./runtime/execution/service"; | 80 | } from "./runtime/execution/service"; |
| 85 | import type { | 81 | import type { |
| 86 | CompleteProviderOauthCallbackResult, | ||
| 87 | CompleteSourceCredentialSetupResult, | 82 | CompleteSourceCredentialSetupResult, |
| 88 | CompleteSourceOAuthSessionResult, | ||
| 89 | ExecutorAddSourceInput, | 83 | ExecutorAddSourceInput, |
| 90 | ExecutorSourceAddResult, | 84 | ExecutorSourceAddResult, |
| 91 | StartSourceOAuthSessionInput, | ||
| 92 | StartSourceOAuthSessionResult, | ||
| 93 | } from "./runtime/sources/source-auth-service"; | 85 | } from "./runtime/sources/source-auth-service"; |
| 94 | 86 | ||
| 95 | type DistributiveOmit<T, Keys extends PropertyKey> = T extends unknown | 87 | type DistributiveOmit<T, Keys extends PropertyKey> = T extends unknown |
| @@ -110,11 +102,6 @@ export type ExecutorSourceInput = DistributiveOmit< · 14 unmodified lines | |||
| 110 | "scopeId" | "actorScopeId" | "executionId" | "interactionId" | 102 | "scopeId" | "actorScopeId" | "executionId" | "interactionId" |
| 111 | >; | 103 | >; |
| 112 | 104 | ||
| 113 | export type ExecutorSourceOAuthInput = DistributiveOmit< | ||
| 114 | StartSourceOAuthSessionInput, | ||
| 115 | "scopeId" | "actorScopeId" | ||
| 116 | >; | ||
| 117 | |||
| 118 | export type ExecutorEffect = { | 105 | export type ExecutorEffect = { |
| 119 | runtime: ExecutorRuntime; | 106 | runtime: ExecutorRuntime; |
| 120 | installation: LocalInstallation; | 107 | installation: LocalInstallation; |
| @@ -187,10 +174,6 @@ export type ExecutorEffect = { · 66 unmodified lines | |||
| 187 | baseUrl?: string | null; | 174 | baseUrl?: string | null; |
| 188 | }, | 175 | }, |
| 189 | ) => Effect.Effect<ExecutorSourceAddResult, Error, never>; | 176 | ) => Effect.Effect<ExecutorSourceAddResult, Error, never>; |
| 190 | discover: (input: { | ||
| 191 | url: string; | ||
| 192 | probeAuth?: Parameters<typeof discoverSource>[0]["probeAuth"]; | ||
| 193 | }) => ProvidedEffect<ReturnType<typeof discoverSource>>; | ||
| 194 | list: () => ProvidedEffect<ReturnType<typeof listSources>>; | 177 | list: () => ProvidedEffect<ReturnType<typeof listSources>>; |
| 195 | create: ( | 178 | create: ( |
| 196 | payload: CreateSourcePayload, | 179 | payload: CreateSourcePayload, |
| @@ -218,41 +201,6 @@ export type ExecutorEffect = { · 21 unmodified lines | |||
| 218 | ReturnType<typeof discoverSourceInspectionTools> | 201 | ReturnType<typeof discoverSourceInspectionTools> |
| 219 | >; | 202 | >; |
| 220 | }; | 203 | }; |
| 221 | oauthClients: { | ||
| 222 | list: ( | ||
| 223 | providerKey: string, | ||
| 224 | ) => Effect.Effect<ReadonlyArray<ScopeOauthClient>, Error, never>; | ||
| 225 | create: ( | ||
| 226 | payload: CreateScopeOauthClientPayload, | ||
| 227 | ) => Effect.Effect<ScopeOauthClient, Error, never>; | ||
| 228 | remove: ( | ||
| 229 | oauthClientId: ScopeOauthClient["id"], | ||
| 230 | ) => Effect.Effect<boolean, Error, never>; | ||
| 231 | }; | ||
| 232 | providerGrants: { | ||
| 233 | remove: ( | ||
| 234 | grantId: ProviderAuthGrant["id"], | ||
| 235 | ) => Effect.Effect<boolean, Error, never>; | ||
| 236 | }; | ||
| 237 | }; | ||
| 238 | oauth: { | ||
| 239 | startSourceAuth: ( | ||
| 240 | input: ExecutorSourceOAuthInput, | ||
| 241 | ) => Effect.Effect<StartSourceOAuthSessionResult, Error, never>; | ||
| 242 | completeSourceAuth: (input: { | ||
| 243 | state: string; | ||
| 244 | code?: string | null; | ||
| 245 | error?: string | null; | ||
| 246 | errorDescription?: string | null; | ||
| 247 | }) => Effect.Effect<CompleteSourceOAuthSessionResult, Error, never>; | ||
| 248 | completeProviderCallback: (input: { | ||
| 249 | scopeId?: ScopeId; | ||
| 250 | actorScopeId?: ScopeId | null; | ||
| 251 | state: string; | ||
| 252 | code?: string | null; | ||
| 253 | error?: string | null; | ||
| 254 | errorDescription?: string | null; | ||
| 255 | }) => Effect.Effect<CompleteProviderOauthCallbackResult, Error, never>; | ||
| 256 | }; | 204 | }; |
| 257 | executions: { | 205 | executions: { |
| 258 | create: ( | 206 | create: ( |
| @@ -366,7 +314,6 @@ const fromRuntime = (runtime: ExecutorRuntime): ExecutorEffect => { · 107 unmodified lines | |||
| 366 | options, | 314 | options, |
| 367 | ); | 315 | ); |
| 368 | }), | 316 | }), |
| 369 | discover: (input) => provide(discoverSource(input)), | ||
| 370 | list: () => provide(listSources({ scopeId, actorScopeId })), | 317 | list: () => provide(listSources({ scopeId, actorScopeId })), |
| 371 | create: (payload) => provide(createSource({ scopeId, actorScopeId, payload })), | 318 | create: (payload) => provide(createSource({ scopeId, actorScopeId, payload })), |
| 372 | get: (sourceId) => provide(getSource({ scopeId, sourceId, actorScopeId })), | 319 | get: (sourceId) => provide(getSource({ scopeId, sourceId, actorScopeId })), |
| @@ -395,67 +342,6 @@ const fromRuntime = (runtime: ExecutorRuntime): ExecutorEffect => { · 22 unmodified lines | |||
| 395 | }), | 342 | }), |
| 396 | ), | 343 | ), |
| 397 | }, | 344 | }, |
| 398 | oauthClients: { | ||
| 399 | list: (providerKey) => | ||
| 400 | provideSourceAuth((service) => | ||
| 401 | service.listScopeOauthClients({ | ||
| 402 | scopeId, | ||
| 403 | providerKey, | ||
| 404 | }), | ||
| 405 | ), | ||
| 406 | create: (payload) => | ||
| 407 | provideSourceAuth((service) => | ||
| 408 | service.createScopeOauthClient({ | ||
| 409 | scopeId, | ||
| 410 | providerKey: payload.providerKey, | ||
| 411 | label: payload.label, | ||
| 412 | oauthClient: payload.oauthClient, | ||
| 413 | }), | ||
| 414 | ), | ||
| 415 | remove: (oauthClientId) => | ||
| 416 | provideSourceAuth((service) => | ||
| 417 | service.removeScopeOauthClient({ | ||
| 418 | scopeId, | ||
| 419 | oauthClientId, | ||
| 420 | }), | ||
| 421 | ), | ||
| 422 | }, | ||
| 423 | providerGrants: { | ||
| 424 | remove: (grantId) => | ||
| 425 | provideSourceAuth((service) => | ||
| 426 | service.removeProviderAuthGrant({ | ||
| 427 | scopeId, | ||
| 428 | grantId, | ||
| 429 | }), | ||
| 430 | ), | ||
| 431 | }, | ||
| 432 | }, | ||
| 433 | oauth: { | ||
| 434 | startSourceAuth: (input) => | ||
| 435 | provideSourceAuth((service) => | ||
| 436 | service.startSourceOAuthSession({ | ||
| 437 | ...input, | ||
| 438 | scopeId, | ||
| 439 | actorScopeId, | ||
| 440 | }), | ||
| 441 | ), | ||
| 442 | completeSourceAuth: ({ state, code, error, errorDescription }) => | ||
| 443 | provideSourceAuth((service) => | ||
| 444 | service.completeSourceOAuthSession({ | ||
| 445 | state, | ||
| 446 | code, | ||
| 447 | error, | ||
| 448 | errorDescription, | ||
| 449 | }), | ||
| 450 | ), | ||
| 451 | completeProviderCallback: (input) => | ||
| 452 | provideSourceAuth((service) => | ||
| 453 | service.completeProviderOauthCallback({ | ||
| 454 | ...input, | ||
| 455 | scopeId: input.scopeId ?? scopeId, | ||
| 456 | actorScopeId: input.actorScopeId ?? actorScopeId, | ||
| 457 | }), | ||
| 458 | ), | ||
| 459 | }, | 345 | }, |
| 460 | executions: { | 346 | executions: { |
| 461 | create: (payload) => | 347 | create: (payload) => |
| @@ -5,7 +5,6 @@ import { · 4 unmodified lines | |||
| 5 | type CreateExecutorEffectOptions, | 5 | type CreateExecutorEffectOptions, |
| 6 | type ExecutorEffect, | 6 | type ExecutorEffect, |
| 7 | type ExecutorSourceInput, | 7 | type ExecutorSourceInput, |
| 8 | type ExecutorSourceOAuthInput, | ||
| 9 | } from "./executor-effect"; | 8 | } from "./executor-effect"; |
| 10 | 9 | ||
| 11 | type Promiseify<T> = T extends Effect.Effect<infer A, any, any> | 10 | type Promiseify<T> = T extends Effect.Effect<infer A, any, any> |
| @@ -22,7 +21,6 @@ export type Executor = Omit<Promiseify<ExecutorEffect>, "runtime">; · 10 unmodified lines | |||
| 22 | export type CreateExecutorOptions = CreateExecutorEffectOptions; | 21 | export type CreateExecutorOptions = CreateExecutorEffectOptions; |
| 23 | export type { | 22 | export type { |
| 24 | ExecutorSourceInput, | 23 | ExecutorSourceInput, |
| 25 | ExecutorSourceOAuthInput, | ||
| 26 | }; | 24 | }; |
| 27 | 25 | ||
| 28 | const toPromiseExecutor = (executor: ExecutorEffect): Executor => { | 26 | const toPromiseExecutor = (executor: ExecutorEffect): Executor => { |
| @@ -60,7 +58,6 @@ const toPromiseExecutor = (executor: ExecutorEffect): Executor => { · 31 unmodified lines | |||
| 60 | }, | 58 | }, |
| 61 | sources: { | 59 | sources: { |
| 62 | add: (input, options) => run(executor.sources.add(input, options)), | 60 | add: (input, options) => run(executor.sources.add(input, options)), |
| 63 | discover: (input) => run(executor.sources.discover(input)), | ||
| 64 | list: () => run(executor.sources.list()), | 61 | list: () => run(executor.sources.list()), |
| 65 | create: (payload) => run(executor.sources.create(payload)), | 62 | create: (payload) => run(executor.sources.create(payload)), |
| 66 | get: (sourceId) => run(executor.sources.get(sourceId)), | 63 | get: (sourceId) => run(executor.sources.get(sourceId)), |
| @@ -72,24 +69,6 @@ const toPromiseExecutor = (executor: ExecutorEffect): Executor => { · 5 unmodified lines | |||
| 72 | tool: (input) => run(executor.sources.inspection.tool(input)), | 69 | tool: (input) => run(executor.sources.inspection.tool(input)), |
| 73 | discover: (input) => run(executor.sources.inspection.discover(input)), | 70 | discover: (input) => run(executor.sources.inspection.discover(input)), |
| 74 | }, | 71 | }, |
| 75 | oauthClients: { | ||
| 76 | list: (providerKey) => | ||
| 77 | run(executor.sources.oauthClients.list(providerKey)), | ||
| 78 | create: (payload) => run(executor.sources.oauthClients.create(payload)), | ||
| 79 | remove: (oauthClientId) => | ||
| 80 | run(executor.sources.oauthClients.remove(oauthClientId)), | ||
| 81 | }, | ||
| 82 | providerGrants: { | ||
| 83 | remove: (grantId) => | ||
| 84 | run(executor.sources.providerGrants.remove(grantId)), | ||
| 85 | }, | ||
| 86 | }, | ||
| 87 | oauth: { | ||
| 88 | startSourceAuth: (input) => run(executor.oauth.startSourceAuth(input)), | ||
| 89 | completeSourceAuth: (input) => | ||
| 90 | run(executor.oauth.completeSourceAuth(input)), | ||
| 91 | completeProviderCallback: (input) => | ||
| 92 | run(executor.oauth.completeProviderCallback(input)), | ||
| 93 | }, | 72 | }, |
| 94 | executions: { | 73 | executions: { |
| 95 | create: (payload) => run(executor.executions.create(payload)), | 74 | create: (payload) => run(executor.executions.create(payload)), |
| @@ -1,6 +1,5 @@ | |||
| 1 | export * from "./local/operations"; | 1 | export * from "./local/operations"; |
| 2 | export * from "./local/secrets"; | 2 | export * from "./local/secrets"; |
| 3 | export * from "./policies/operations"; | 3 | export * from "./policies/operations"; |
| 4 | export * from "./sources/discovery"; | ||
| 5 | export * from "./sources/inspection"; | 4 | export * from "./sources/inspection"; |
| 6 | export * from "./sources/operations"; | 5 | export * from "./sources/operations"; |
| @@ -111,7 +111,6 @@ export * from "./sources/source-store"; · 110 unmodified lines | |||
| 111 | export * from "./executor-state-store"; | 111 | export * from "./executor-state-store"; |
| 112 | export * from "./execution/scope/environment"; | 112 | export * from "./execution/scope/environment"; |
| 113 | export * from "../sources/inspection"; | 113 | export * from "../sources/inspection"; |
| 114 | export * from "../sources/discovery"; | ||
| 115 | export * from "./execution/service"; | 114 | export * from "./execution/service"; |
| 116 | export type { | 115 | export type { |
| 117 | CreateScopeInternalToolMap, | 116 | CreateScopeInternalToolMap, |
| @@ -3,14 +3,11 @@ import type { · 2 unmodified lines | |||
| 3 | SecretRef, | 3 | SecretRef, |
| 4 | } from "@executor/source-core"; | 4 | } from "@executor/source-core"; |
| 5 | import { | 5 | import { |
| 6 | type ProviderAuthGrant, | ||
| 7 | type ScopeId, | 6 | type ScopeId, |
| 8 | type ScopeOauthClient, | ||
| 9 | type Source, | 7 | type Source, |
| 10 | type SourceAuthSession, | 8 | type SourceAuthSession, |
| 11 | SourceAuthSessionIdSchema, | 9 | SourceAuthSessionIdSchema, |
| 12 | SourceSchema, | 10 | SourceSchema, |
| 13 | type SourceOauthClientInput, | ||
| 14 | } from "#schema"; | 11 | } from "#schema"; |
| 15 | import * as Context from "effect/Context"; | 12 | import * as Context from "effect/Context"; |
| 16 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| @@ -92,46 +89,11 @@ export type ExecutorAddSourceInput = RegisteredExecutorAddSourceInput & { · 75 unmodified lines | |||
| 92 | interactionId: SourceAuthSession["interactionId"]; | 89 | interactionId: SourceAuthSession["interactionId"]; |
| 93 | }; | 90 | }; |
| 94 | 91 | ||
| 95 | export type CreateScopeOauthClientInput = { | ||
| 96 | scopeId: ScopeId; | ||
| 97 | providerKey: string; | ||
| 98 | label?: string | null; | ||
| 99 | oauthClient: SourceOauthClientInput; | ||
| 100 | }; | ||
| 101 | |||
| 102 | export type SourceOAuthProviderInput = { | ||
| 103 | providerKey: string; | ||
| 104 | config?: Record<string, unknown> | null; | ||
| 105 | }; | ||
| 106 | |||
| 107 | export type StartSourceOAuthSessionInput = { | ||
| 108 | scopeId: ScopeId; | ||
| 109 | actorScopeId?: ScopeId | null; | ||
| 110 | provider: SourceOAuthProviderInput; | ||
| 111 | baseUrl?: string | null; | ||
| 112 | displayName?: string | null; | ||
| 113 | }; | ||
| 114 | |||
| 115 | export type StartSourceOAuthSessionResult = { | ||
| 116 | sessionId: SourceAuthSession["id"]; | ||
| 117 | authorizationUrl: string; | ||
| 118 | }; | ||
| 119 | |||
| 120 | export type CompleteSourceOAuthSessionResult = { | ||
| 121 | sessionId: SourceAuthSession["id"]; | ||
| 122 | auth: Extract<Source["auth"], { kind: "oauth2" }>; | ||
| 123 | }; | ||
| 124 | |||
| 125 | export type CompleteSourceCredentialSetupResult = { | 92 | export type CompleteSourceCredentialSetupResult = { |
| 126 | sessionId: SourceAuthSession["id"]; | 93 | sessionId: SourceAuthSession["id"]; |
| 127 | source: Source; | 94 | source: Source; |
| 128 | }; | 95 | }; |
| 129 | 96 | ||
| 130 | export type CompleteProviderOauthCallbackResult = { | ||
| 131 | sessionId: SourceAuthSession["id"]; | ||
| 132 | sources: ReadonlyArray<Source>; | ||
| 133 | }; | ||
| 134 | |||
| 135 | type RuntimeSourceAuthServiceShape = { | 97 | type RuntimeSourceAuthServiceShape = { |
| 136 | getSourceById: (input: { | 98 | getSourceById: (input: { |
| 137 | scopeId: ScopeId; | 99 | scopeId: ScopeId; |
| @@ -150,38 +112,6 @@ type RuntimeSourceAuthServiceShape = { · 12 unmodified lines | |||
| 150 | baseUrl?: string | null; | 112 | baseUrl?: string | null; |
| 151 | }, | 113 | }, |
| 152 | ) => Effect.Effect<ExecutorSourceAddResult, Error, ScopeStorageServices>; | 114 | ) => Effect.Effect<ExecutorSourceAddResult, Error, ScopeStorageServices>; |
| 153 | listScopeOauthClients: (input: { | ||
| 154 | scopeId: ScopeId; | ||
| 155 | providerKey: string; | ||
| 156 | }) => Effect.Effect<readonly ScopeOauthClient[], Error, ScopeStorageServices>; | ||
| 157 | createScopeOauthClient: ( | ||
| 158 | input: CreateScopeOauthClientInput, | ||
| 159 | ) => Effect.Effect<ScopeOauthClient, Error, ScopeStorageServices>; | ||
| 160 | removeScopeOauthClient: (input: { | ||
| 161 | scopeId: ScopeId; | ||
| 162 | oauthClientId: ScopeOauthClient["id"]; | ||
| 163 | }) => Effect.Effect<boolean, Error, ScopeStorageServices>; | ||
| 164 | removeProviderAuthGrant: (input: { | ||
| 165 | scopeId: ScopeId; | ||
| 166 | grantId: ProviderAuthGrant["id"]; | ||
| 167 | }) => Effect.Effect<boolean, Error, ScopeStorageServices>; | ||
| 168 | startSourceOAuthSession: ( | ||
| 169 | input: StartSourceOAuthSessionInput, | ||
| 170 | ) => Effect.Effect<StartSourceOAuthSessionResult, Error, never>; | ||
| 171 | completeSourceOAuthSession: (input: { | ||
| 172 | state: string; | ||
| 173 | code?: string | null; | ||
| 174 | error?: string | null; | ||
| 175 | errorDescription?: string | null; | ||
| 176 | }) => Effect.Effect<CompleteSourceOAuthSessionResult, Error, ScopeStorageServices>; | ||
| 177 | completeProviderOauthCallback: (input: { | ||
| 178 | scopeId: ScopeId; | ||
| 179 | actorScopeId?: ScopeId | null; | ||
| 180 | state: string; | ||
| 181 | code?: string | null; | ||
| 182 | error?: string | null; | ||
| 183 | errorDescription?: string | null; | ||
| 184 | }) => Effect.Effect<CompleteProviderOauthCallbackResult, Error, ScopeStorageServices>; | ||
| 185 | completeSourceCredentialSetup: (input: { | 115 | completeSourceCredentialSetup: (input: { |
| 186 | scopeId: ScopeId; | 116 | scopeId: ScopeId; |
| 187 | sourceId: Source["id"]; | 117 | sourceId: Source["id"]; |
| @@ -228,16 +158,6 @@ export const createRuntimeSourceAuthService = ( · 40 unmodified lines | |||
| 228 | }), | 158 | }), |
| 229 | addExecutorSource: () => | 159 | addExecutorSource: () => |
| 230 | provideLocalWorkspace(disabledSourcePlugins("addExecutorSource")), | 160 | provideLocalWorkspace(disabledSourcePlugins("addExecutorSource")), |
| 231 | listScopeOauthClients: () => provideLocalWorkspace(Effect.succeed([])), | ||
| 232 | createScopeOauthClient: () => | ||
| 233 | provideLocalWorkspace(disabledSourcePlugins("createScopeOauthClient")), | ||
| 234 | removeScopeOauthClient: () => provideLocalWorkspace(Effect.succeed(false)), | ||
| 235 | removeProviderAuthGrant: () => provideLocalWorkspace(Effect.succeed(false)), | ||
| 236 | startSourceOAuthSession: () => disabledSourcePlugins("startSourceOAuthSession"), | ||
| 237 | completeSourceOAuthSession: () => | ||
| 238 | provideLocalWorkspace(disabledSourcePlugins("completeSourceOAuthSession")), | ||
| 239 | completeProviderOauthCallback: () => | ||
| 240 | provideLocalWorkspace(disabledSourcePlugins("completeProviderOauthCallback")), | ||
| 241 | completeSourceCredentialSetup: () => | 161 | completeSourceCredentialSetup: () => |
| 242 | provideLocalWorkspace(disabledSourcePlugins("completeSourceCredentialSetup")), | 162 | provideLocalWorkspace(disabledSourcePlugins("completeSourceCredentialSetup")), |
| 243 | }; | 163 | }; |
| @@ -1,278 +0,0 @@ | |||
| 1 | import { | ||
| 2 | createServer, | ||
| 3 | type IncomingMessage, | ||
| 4 | type ServerResponse, | ||
| 5 | } from "node:http"; | ||
| 6 | |||
| 7 | import { | ||
| 8 | describe, | ||
| 9 | expect, | ||
| 10 | it, | ||
| 11 | } from "@effect/vitest"; | ||
| 12 | import * as Effect from "effect/Effect"; | ||
| 13 | |||
| 14 | import { | ||
| 15 | discoverSource, | ||
| 16 | } from "../../sources/discovery"; | ||
| 17 | |||
| 18 | type TestServer = { | ||
| 19 | url: string; | ||
| 20 | }; | ||
| 21 | |||
| 22 | const toError = (cause: unknown): Error => | ||
| 23 | cause instanceof Error ? cause : new Error(String(cause)); | ||
| 24 | |||
| 25 | const makeServer = ( | ||
| 26 | handler: (input: { | ||
| 27 | request: IncomingMessage; | ||
| 28 | response: ServerResponse<IncomingMessage>; | ||
| 29 | url: string; | ||
| 30 | }) => void, | ||
| 31 | ) => | ||
| 32 | Effect.acquireRelease( | ||
| 33 | Effect.tryPromise({ | ||
| 34 | try: () => | ||
| 35 | new Promise<{ | ||
| 36 | server: ReturnType<typeof createServer>; | ||
| 37 | sockets: Set<import("node:net").Socket>; | ||
| 38 | url: string; | ||
| 39 | }>((resolve, reject) => { | ||
| 40 | let serverUrl = ""; | ||
| 41 | const server = createServer((request, response) => | ||
| 42 | handler({ | ||
| 43 | request, | ||
| 44 | response, | ||
| 45 | url: serverUrl, | ||
| 46 | }), | ||
| 47 | ); | ||
| 48 | const sockets = new Set<import("node:net").Socket>(); | ||
| 49 | |||
| 50 | server.on("connection", (socket) => { | ||
| 51 | sockets.add(socket); | ||
| 52 | socket.on("close", () => { | ||
| 53 | sockets.delete(socket); | ||
| 54 | }); | ||
| 55 | }); | ||
| 56 | |||
| 57 | server.once("error", reject); | ||
| 58 | server.listen(0, "127.0.0.1", () => { | ||
| 59 | const address = server.address(); | ||
| 60 | if (!address || typeof address === "string") { | ||
| 61 | reject(new Error("Failed to resolve test server address")); | ||
| 62 | return; | ||
| 63 | } | ||
| 64 | |||
| 65 | serverUrl = `http://127.0.0.1:${address.port}`; | ||
| 66 | resolve({ | ||
| 67 | server, | ||
| 68 | sockets, | ||
| 69 | url: serverUrl, | ||
| 70 | }); | ||
| 71 | }); | ||
| 72 | }), | ||
| 73 | catch: toError, | ||
| 74 | }), | ||
| 75 | ({ server, sockets }) => | ||
| 76 | Effect.tryPromise({ | ||
| 77 | try: () => | ||
| 78 | new Promise<void>((resolve, reject) => { | ||
| 79 | server.closeAllConnections?.(); | ||
| 80 | sockets.forEach((socket) => socket.destroy()); | ||
| 81 | server.close((error) => { | ||
| 82 | if (error) { | ||
| 83 | reject(error); | ||
| 84 | return; | ||
| 85 | } | ||
| 86 | resolve(); | ||
| 87 | }); | ||
| 88 | }), | ||
| 89 | catch: toError, | ||
| 90 | }), | ||
| 91 | ).pipe(Effect.map(({ url }) => ({ url }) satisfies TestServer)); | ||
| 92 | |||
| 93 | describe("source-discovery", () => { | ||
| 94 | it.scoped( | ||
| 95 | "detects OpenAPI and infers bearer auth from security schemes", | ||
| 96 | () => | ||
| 97 | Effect.gen(function* () { | ||
| 98 | const server = yield* makeServer(({ request, response, url }) => { | ||
| 99 | if (request.url !== "/openapi.json") { | ||
| 100 | response.statusCode = 404; | ||
| 101 | response.end(); | ||
| 102 | return; | ||
| 103 | } | ||
| 104 | |||
| 105 | if (request.headers.authorization !== "Bearer top-secret") { | ||
| 106 | response.statusCode = 401; | ||
| 107 | response.setHeader("www-authenticate", 'Bearer realm="spec"'); | ||
| 108 | response.end("Unauthorized"); | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | response.statusCode = 200; | ||
| 113 | response.setHeader("content-type", "application/json"); | ||
| 114 | response.end( | ||
| 115 | JSON.stringify({ | ||
| 116 | openapi: "3.0.3", | ||
| 117 | info: { | ||
| 118 | title: "Secure Example API", | ||
| 119 | version: "1.0.0", | ||
| 120 | }, | ||
| 121 | servers: [{ url: `${url}/api` }], | ||
| 122 | components: { | ||
| 123 | securitySchemes: { | ||
| 124 | bearerAuth: { | ||
| 125 | type: "http", | ||
| 126 | scheme: "bearer", | ||
| 127 | }, | ||
| 128 | }, | ||
| 129 | }, | ||
| 130 | security: [{ bearerAuth: [] }], | ||
| 131 | paths: { | ||
| 132 | "/widgets": { | ||
| 133 | get: { | ||
| 134 | operationId: "widgets/list", | ||
| 135 | responses: { | ||
| 136 | 200: { | ||
| 137 | description: "ok", | ||
| 138 | }, | ||
| 139 | }, | ||
| 140 | }, | ||
| 141 | }, | ||
| 142 | }, | ||
| 143 | }), | ||
| 144 | ); | ||
| 145 | }); | ||
| 146 | |||
| 147 | const result = yield* discoverSource({ | ||
| 148 | url: `${server.url}/openapi.json`, | ||
| 149 | probeAuth: { | ||
| 150 | kind: "bearer", | ||
| 151 | token: "top-secret", | ||
| 152 | }, | ||
| 153 | }); | ||
| 154 | |||
| 155 | expect(result.detectedKind).toBe("openapi"); | ||
| 156 | expect(result.authInference.suggestedKind).toBe("bearer"); | ||
| 157 | expect(result.authInference.supported).toBe(true); | ||
| 158 | expect(result.authInference.headerName).toBe("Authorization"); | ||
| 159 | expect(result.specUrl).toBe(`${server.url}/openapi.json`); | ||
| 160 | expect(result.endpoint).toBe(`${server.url}/api`); | ||
| 161 | expect(result.toolCount).toBe(1); | ||
| 162 | }), | ||
| 163 | ); | ||
| 164 | |||
| 165 | it.scoped("detects GraphQL from successful introspection", () => | ||
| 166 | Effect.gen(function* () { | ||
| 167 | const server = yield* makeServer(({ request, response }) => { | ||
| 168 | if (request.url !== "/graphql" || request.method !== "POST") { | ||
| 169 | response.statusCode = 404; | ||
| 170 | response.end(); | ||
| 171 | return; | ||
| 172 | } | ||
| 173 | |||
| 174 | response.statusCode = 200; | ||
| 175 | response.setHeader("content-type", "application/json"); | ||
| 176 | response.end( | ||
| 177 | JSON.stringify({ | ||
| 178 | data: { | ||
| 179 | __schema: { | ||
| 180 | queryType: { | ||
| 181 | name: "Query", | ||
| 182 | }, | ||
| 183 | }, | ||
| 184 | }, | ||
| 185 | }), | ||
| 186 | ); | ||
| 187 | }); | ||
| 188 | |||
| 189 | const result = yield* discoverSource({ | ||
| 190 | url: `${server.url}/graphql`, | ||
| 191 | }); | ||
| 192 | |||
| 193 | expect(result.detectedKind).toBe("graphql"); | ||
| 194 | expect(result.confidence).toBe("high"); | ||
| 195 | expect(result.authInference.suggestedKind).toBe("none"); | ||
| 196 | expect(result.specUrl).toBeNull(); | ||
| 197 | }), | ||
| 198 | ); | ||
| 199 | |||
| 200 | it.scoped("detects Google Discovery documents", () => | ||
| 201 | Effect.gen(function* () { | ||
| 202 | const server = yield* makeServer(({ request, response, url }) => { | ||
| 203 | if (request.url !== "/gmail/$discovery/rest?version=v1") { | ||
| 204 | response.statusCode = 404; | ||
| 205 | response.end(); | ||
| 206 | return; | ||
| 207 | } | ||
| 208 | |||
| 209 | response.statusCode = 200; | ||
| 210 | response.setHeader("content-type", "application/json"); | ||
| 211 | response.end( | ||
| 212 | JSON.stringify({ | ||
| 213 | name: "gmail", | ||
| 214 | version: "v1", | ||
| 215 | title: "Gmail API", | ||
| 216 | description: "Access Gmail mailboxes and settings.", | ||
| 217 | rootUrl: `${url}/`, | ||
| 218 | servicePath: "gmail/v1/users/", | ||
| 219 | auth: { | ||
| 220 | oauth2: { | ||
| 221 | scopes: { | ||
| 222 | "https://www.googleapis.com/auth/gmail.readonly": { | ||
| 223 | description: "View your email messages and settings", | ||
| 224 | }, | ||
| 225 | }, | ||
| 226 | }, | ||
| 227 | }, | ||
| 228 | methods: { | ||
| 229 | getProfile: { | ||
| 230 | id: "gmail.users.getProfile", | ||
| 231 | path: "{userId}/profile", | ||
| 232 | httpMethod: "GET", | ||
| 233 | parameters: { | ||
| 234 | userId: { | ||
| 235 | type: "string", | ||
| 236 | required: true, | ||
| 237 | location: "path", | ||
| 238 | }, | ||
| 239 | }, | ||
| 240 | response: { | ||
| 241 | $ref: "Profile", | ||
| 242 | }, | ||
| 243 | }, | ||
| 244 | }, | ||
| 245 | schemas: { | ||
| 246 | Profile: { | ||
| 247 | id: "Profile", | ||
| 248 | type: "object", | ||
| 249 | properties: { | ||
| 250 | emailAddress: { | ||
| 251 | type: "string", | ||
| 252 | }, | ||
| 253 | }, | ||
| 254 | }, | ||
| 255 | }, | ||
| 256 | }), | ||
| 257 | ); | ||
| 258 | }); | ||
| 259 | |||
| 260 | const result = yield* discoverSource({ | ||
| 261 | url: `${server.url}/gmail/$discovery/rest?version=v1`, | ||
| 262 | }); | ||
| 263 | |||
| 264 | expect(result.detectedKind).toBe("google_discovery"); | ||
| 265 | expect(result.specUrl).toBe( | ||
| 266 | `${server.url}/gmail/$discovery/rest?version=v1`, | ||
| 267 | ); | ||
| 268 | expect(result.endpoint).toBe(`${server.url}/gmail/v1/users/`); | ||
| 269 | expect(result.name).toBe("Gmail API"); | ||
| 270 | expect(result.namespace).toBe("gmail"); | ||
| 271 | expect(result.authInference.suggestedKind).toBe("oauth2"); | ||
| 272 | expect(result.authInference.oauthScopes).toEqual([ | ||
| 273 | "https://www.googleapis.com/auth/gmail.readonly", | ||
| 274 | ]); | ||
| 275 | expect(result.toolCount).toBe(1); | ||
| 276 | }), | ||
| 277 | ); | ||
| 278 | }); | ||
| @@ -23,22 +23,6 @@ export { · 22 unmodified lines | |||
| 23 | type SourceCatalogKind, | 23 | type SourceCatalogKind, |
| 24 | type SourceCatalogVisibility, | 24 | type SourceCatalogVisibility, |
| 25 | } from "./models/source-catalog"; | 25 | } from "./models/source-catalog"; |
| 26 | export { | ||
| 27 | SourceAuthInferenceSchema, | ||
| 28 | SourceDiscoveryAuthKindSchema, | ||
| 29 | SourceDiscoveryAuthParameterLocationSchema, | ||
| 30 | SourceDiscoveryConfidenceSchema, | ||
| 31 | SourceDiscoveryKindSchema, | ||
| 32 | SourceDiscoveryResultSchema, | ||
| 33 | SourceProbeAuthSchema, | ||
| 34 | type SourceAuthInference, | ||
| 35 | type SourceDiscoveryAuthKind, | ||
| 36 | type SourceDiscoveryAuthParameterLocation, | ||
| 37 | type SourceDiscoveryConfidence, | ||
| 38 | type SourceDiscoveryKind, | ||
| 39 | type SourceDiscoveryResult, | ||
| 40 | type SourceProbeAuth, | ||
| 41 | } from "./models/source-discovery"; | ||
| 42 | export { | 26 | export { |
| 43 | AuthArtifactKindSchema, | 27 | AuthArtifactKindSchema, |
| 44 | AuthArtifactSlotSchema, | 28 | AuthArtifactSlotSchema, |
| @@ -16,5 +16,4 @@ export * from "./models/local-installation"; · 15 unmodified lines | |||
| 16 | export * from "./models/local-config"; | 16 | export * from "./models/local-config"; |
| 17 | export * from "./models/execution"; | 17 | export * from "./models/execution"; |
| 18 | export * from "./models/source-inspection"; | 18 | export * from "./models/source-inspection"; |
| 19 | export * from "./models/source-discovery"; | ||
| 20 | export * from "./models/code-migration"; | 19 | export * from "./models/code-migration"; |
| @@ -1,16 +0,0 @@ | |||
| 1 | export { | ||
| 2 | SourceAuthInferenceSchema, | ||
| 3 | SourceDiscoveryAuthKindSchema, | ||
| 4 | SourceDiscoveryAuthParameterLocationSchema, | ||
| 5 | SourceDiscoveryConfidenceSchema, | ||
| 6 | SourceDiscoveryKindSchema, | ||
| 7 | SourceDiscoveryResultSchema, | ||
| 8 | SourceProbeAuthSchema, | ||
| 9 | type SourceAuthInference, | ||
| 10 | type SourceDiscoveryAuthKind, | ||
| 11 | type SourceDiscoveryAuthParameterLocation, | ||
| 12 | type SourceDiscoveryConfidence, | ||
| 13 | type SourceDiscoveryKind, | ||
| 14 | type SourceDiscoveryResult, | ||
| 15 | type SourceProbeAuth, | ||
| 16 | } from "@executor/source-core"; | ||
| @@ -3,10 +3,8 @@ import { · 2 unmodified lines | |||
| 3 | JsonObjectSchema, | 3 | JsonObjectSchema, |
| 4 | ProviderAuthGrantIdSchema, | 4 | ProviderAuthGrantIdSchema, |
| 5 | SourceAuthSchema, | 5 | SourceAuthSchema, |
| 6 | SourceDiscoveryResultSchema, | ||
| 7 | SourceImportAuthPolicySchema, | 6 | SourceImportAuthPolicySchema, |
| 8 | SourceKindSchema, | 7 | SourceKindSchema, |
| 9 | SourceProbeAuthSchema, | ||
| 10 | SourceSchema, | 8 | SourceSchema, |
| 11 | SourceStatusSchema, | 9 | SourceStatusSchema, |
| 12 | SourceOauthClientInputSchema, | 10 | SourceOauthClientInputSchema, |
| @@ -93,15 +91,7 @@ export type CreateScopeOauthClientPayload = · 80 unmodified lines | |||
| 93 | export const oauthClientIdParam = ScopeOauthClientIdSchema; | 91 | export const oauthClientIdParam = ScopeOauthClientIdSchema; |
| 94 | export const grantIdParam = ProviderAuthGrantIdSchema; | 92 | export const grantIdParam = ProviderAuthGrantIdSchema; |
| 95 | 93 | ||
| 96 | export const DiscoverSourcePayloadSchema = Schema.Struct({ | ||
| 97 | url: TrimmedNonEmptyStringSchema, | ||
| 98 | probeAuth: Schema.optional(SourceProbeAuthSchema), | ||
| 99 | }); | ||
| 100 | |||
| 101 | export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type; | ||
| 102 | |||
| 103 | export { | 94 | export { |
| 104 | SourceDiscoveryResultSchema, | ||
| 105 | ScopeIdSchema, | 95 | ScopeIdSchema, |
| 106 | ScopeOauthClientSchema, | 96 | ScopeOauthClientSchema, |
| 107 | }; | 97 | }; |
| @@ -1,56 +0,0 @@ | |||
| 1 | import * as Effect from "effect/Effect"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | fallbackSourceDiscoveryResult, | ||
| 5 | normalizeSourceDiscoveryUrl, | ||
| 6 | probeHeadersFromAuth, | ||
| 7 | type SourceAdapter, | ||
| 8 | type SourceProbeAuth, | ||
| 9 | type SourceDiscoveryResult, | ||
| 10 | } from "@executor/source-core"; | ||
| 11 | |||
| 12 | import { | ||
| 13 | builtInSourceAdapters, | ||
| 14 | } from "../runtime/sources/source-adapters"; | ||
| 15 | |||
| 16 | type DiscoverableSourceAdapter = SourceAdapter & { | ||
| 17 | detectSource: NonNullable<SourceAdapter["detectSource"]>; | ||
| 18 | }; | ||
| 19 | |||
| 20 | const sourceAdapters: ReadonlyArray<SourceAdapter> = builtInSourceAdapters; | ||
| 21 | |||
| 22 | const discoverableSourceAdapters = sourceAdapters.filter( | ||
| 23 | (adapter): adapter is DiscoverableSourceAdapter => | ||
| 24 | adapter.detectSource !== undefined, | ||
| 25 | ); | ||
| 26 | |||
| 27 | export const discoverSource = (input: { | ||
| 28 | url: string; | ||
| 29 | probeAuth?: SourceProbeAuth | null; | ||
| 30 | }): Effect.Effect<SourceDiscoveryResult, Error, never> => | ||
| 31 | Effect.gen(function* () { | ||
| 32 | const normalizedUrl = yield* Effect.try({ | ||
| 33 | try: () => normalizeSourceDiscoveryUrl(input.url), | ||
| 34 | catch: (cause) => | ||
| 35 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 36 | }); | ||
| 37 | const headers = probeHeadersFromAuth(input.probeAuth); | ||
| 38 | |||
| 39 | const adaptersByPriority = [...discoverableSourceAdapters].sort( | ||
| 40 | (left, right) => | ||
| 41 | (right.discoveryPriority?.({ normalizedUrl }) ?? 0) - | ||
| 42 | (left.discoveryPriority?.({ normalizedUrl }) ?? 0), | ||
| 43 | ); | ||
| 44 | |||
| 45 | for (const adapter of adaptersByPriority) { | ||
| 46 | const detected = yield* adapter.detectSource({ | ||
| 47 | normalizedUrl, | ||
| 48 | headers, | ||
| 49 | }); | ||
| 50 | if (detected) { | ||
| 51 | return detected; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | return fallbackSourceDiscoveryResult(normalizedUrl); | ||
| 56 | }); | ||
| @@ -6,10 +6,7 @@ import { · 5 unmodified lines | |||
| 6 | } from "./source-models"; | 6 | } from "./source-models"; |
| 7 | 7 | ||
| 8 | export const SourceDiscoveryKindSchema = Schema.Literal( | 8 | export const SourceDiscoveryKindSchema = Schema.Literal( |
| 9 | "mcp", | 9 | "plugin", |
| 10 | "openapi", | ||
| 11 | "google_discovery", | ||
| 12 | "graphql", | ||
| 13 | "unknown", | 10 | "unknown", |
| 14 | ); | 11 | ); |
| 15 | 12 | ||
| @@ -215,9 +215,6 @@ export const executeHttpProbe = (input: { · 214 unmodified lines | |||
| 215 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | 215 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), |
| 216 | }); | 216 | }); |
| 217 | 217 | ||
| 218 | export const looksLikeGraphqlEndpoint = (normalizedUrl: string): boolean => | ||
| 219 | /graphql/i.test(new URL(normalizedUrl).pathname); | ||
| 220 | |||
| 221 | export const tryParseJson = (value: string): unknown => { | 218 | export const tryParseJson = (value: string): unknown => { |
| 222 | try { | 219 | try { |
| 223 | return JSON.parse(value) as unknown; | 220 | return JSON.parse(value) as unknown; |
| @@ -244,7 +241,7 @@ export const fallbackSourceDiscoveryResult = ( · 20 unmodified lines | |||
| 244 | ), | 241 | ), |
| 245 | toolCount: null, | 242 | toolCount: null, |
| 246 | warnings: [ | 243 | warnings: [ |
| 247 | "Could not confirm whether the URL is Google Discovery, OpenAPI, GraphQL, or MCP.", | 244 | "Could not confirm whether the URL matches any registered source plugin.", |
| 248 | ], | 245 | ], |
| 249 | }; | 246 | }; |
| 250 | }; | 247 | }; |