Unified · Split
| @@ -57,17 +57,17 @@ They answer: · 56 unmodified lines | |||
| 57 | 57 | ||
| 58 | The operation layer exists because raw upstream documents are not a good direct query surface for search, policy, and invocation lookup. | 58 | The operation layer exists because raw upstream documents are not a good direct query surface for search, policy, and invocation lookup. |
| 59 | 59 | ||
| 60 | ### Credential | 60 | ### Secret material |
| 61 | 61 | ||
| 62 | Credentials are private auth bindings. | 62 | Secret material is private auth state. |
| 63 | 63 | ||
| 64 | They answer: | 64 | It answers: |
| 65 | 65 | ||
| 66 | - who can authenticate for this binding? | 66 | - which private values exist for this workspace actor? |
| 67 | - for which slot? | 67 | - which source bindings refer to them? |
| 68 | - using what auth kind and secret refs? | 68 | - which plugin-owned auth configs use those secret refs? |
| 69 | 69 | ||
| 70 | Credentials are actor-scoped by default because two users in the same workspace may connect different identities to the same source. | 70 | Secret material is actor-scoped by default because two users in the same workspace may connect different identities to the same source. |
| 71 | 71 | ||
| 72 | ## Why the split matters | 72 | ## Why the split matters |
| 73 | 73 | ||
| @@ -115,6 +115,11 @@ Adapters are more specific: · 41 unmodified lines | |||
| 115 | 115 | ||
| 116 | The important distinction is that execution now belongs to adapters. The shared catalog stays broad and stable, while connect/auth/invoke behavior is described through adapter strategies instead of one top-level runtime family enum. | 116 | The important distinction is that execution now belongs to adapters. The shared catalog stays broad and stable, while connect/auth/invoke behavior is described through adapter strategies instead of one top-level runtime family enum. |
| 117 | 117 | ||
| 118 | The same is true for auth behavior: | ||
| 119 | |||
| 120 | - core owns secret storage and private state plumbing | ||
| 121 | - plugins own auth schemas, request shaping, OAuth flows, and refresh logic | ||
| 122 | |||
| 118 | ## Why this is the right shape | 123 | ## Why this is the right shape |
| 119 | 124 | ||
| 120 | The current model is designed so that new support usually means: | 125 | The current model is designed so that new support usually means: |
| @@ -5,11 +5,14 @@ description: "How executor models auth, interactive flows, and actor-scoped cred · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | Auth in executor is not treated as a side detail on the source row. | 6 | Auth in executor is not treated as a side detail on the source row. |
| 7 | 7 | ||
| 8 | It is modeled as its own layer because auth has different ownership and lifecycle semantics from source bindings and recipe materialization. | 8 | The important boundary is: |
| 9 | |||
| 10 | - core owns secret material storage, actor scoping, and private state plumbing | ||
| 11 | - plugins own auth schemas and auth behavior for each source kind | ||
| 9 | 12 | ||
| 10 | ## Auth ownership | 13 | ## Auth ownership |
| 11 | 14 | ||
| 12 | Credentials are scoped to: | 15 | Secret material is scoped to: |
| 13 | 16 | ||
| 14 | - workspace | 17 | - workspace |
| 15 | - source binding | 18 | - source binding |
| @@ -20,7 +23,7 @@ This is important because different users in the same workspace may connect diff · 4 unmodified lines | |||
| 20 | 23 | ||
| 21 | ## Auth slots | 24 | ## Auth slots |
| 22 | 25 | ||
| 23 | Executor distinguishes where auth is used. | 26 | Plugins may distinguish where auth is used. |
| 24 | 27 | ||
| 25 | Important current slots: | 28 | Important current slots: |
| 26 | 29 | ||
| @@ -33,7 +36,7 @@ Why this matters: · 6 unmodified lines | |||
| 33 | - some need auth only to fetch or generate the materialization | 36 | - some need auth only to fetch or generate the materialization |
| 34 | - some may eventually need setup/bootstrap-specific auth | 37 | - some may eventually need setup/bootstrap-specific auth |
| 35 | 38 | ||
| 36 | This is why source auth is more than a single token field. | 39 | This is why plugin-owned source auth is more than a single token field. |
| 37 | 40 | ||
| 38 | ## Static credentials | 41 | ## Static credentials |
| 39 | 42 | ||
| @@ -45,11 +48,11 @@ Examples: · 5 unmodified lines | |||
| 45 | - OAuth access token | 48 | - OAuth access token |
| 46 | - OAuth refresh token | 49 | - OAuth refresh token |
| 47 | 50 | ||
| 48 | The control plane stores secret refs, not raw secret values inline on the source or credential rows. | 51 | The control plane stores secret refs, not raw secret values inline on the source or plugin-owned auth configs. |
| 49 | 52 | ||
| 50 | ## Interactive auth sessions | 53 | ## Interactive auth sessions |
| 51 | 54 | ||
| 52 | Interactive auth, such as OAuth flows, is modeled separately from stored credentials. | 55 | Interactive auth, such as OAuth flows, is modeled separately from stored secret material. |
| 53 | 56 | ||
| 54 | That separation exists because: | 57 | That separation exists because: |
| 55 | 58 | ||
| @@ -57,7 +60,7 @@ That separation exists because: · 1 unmodified line | |||
| 57 | - it may be tied to an interaction or execution | 60 | - it may be tied to an interaction or execution |
| 58 | - it is not the same thing as a resolved credential | 61 | - it is not the same thing as a resolved credential |
| 59 | 62 | ||
| 60 | This is why executor has auth sessions and credentials as separate concepts. | 63 | This is why executor keeps auth sessions separate from stored secret material. |
| 61 | 64 | ||
| 62 | ## Shareable config vs private state | 65 | ## Shareable config vs private state |
| 63 | 66 | ||
| @@ -65,7 +68,7 @@ Project config under `.executor/executor.jsonc` is intended to stay portable and · 1 unmodified line | |||
| 65 | 68 | ||
| 66 | Private auth state does not belong in the workspace tree: | 69 | Private auth state does not belong in the workspace tree: |
| 67 | 70 | ||
| 68 | - actor-scoped credentials | 71 | - actor-scoped secret material |
| 69 | - OAuth sessions | 72 | - OAuth sessions |
| 70 | - refresh leases | 73 | - refresh leases |
| 71 | - secret material | 74 | - secret material |
| @@ -97,17 +100,17 @@ The likely next auth-layer primitive is an auth provider or auth material resolv · 25 unmodified lines | |||
| 97 | - produces runtime auth material on demand | 100 | - produces runtime auth material on demand |
| 98 | - caches it with expiry metadata | 101 | - caches it with expiry metadata |
| 99 | 102 | ||
| 100 | That is broader than sources and should be reusable by future source adapters, tool providers, and code bundles. | 103 | That is broader than sources and should be reusable by future plugins, tool providers, and code bundles. |
| 101 | 104 | ||
| 102 | ## Why auth is separated from recipes | 105 | ## Why auth is separated from recipes |
| 103 | 106 | ||
| 104 | Recipes are shareable integration definitions. | 107 | Recipes are shareable integration definitions. |
| 105 | Credentials are private installation-specific state. | 108 | Secret material is private installation-specific state. |
| 106 | 109 | ||
| 107 | That separation is important both for correctness and for shareability: | 110 | That separation is important both for correctness and for shareability: |
| 108 | 111 | ||
| 109 | - a recipe can be portable | 112 | - a recipe can be portable |
| 110 | - a source binding can be local | 113 | - a source binding can be local |
| 111 | - credentials stay private | 114 | - secret material stays private |
| 112 | 115 | ||
| 113 | This is the main reason auth is not modeled as part of the shareable recipe layer. | 116 | This is the main reason auth is not modeled as part of the shareable recipe layer. |
| @@ -36,6 +36,12 @@ The happy path for a new integration is: · 35 unmodified lines | |||
| 36 | 36 | ||
| 37 | The adapter should absorb provider-specific logic so the core does not have to. | 37 | The adapter should absorb provider-specific logic so the core does not have to. |
| 38 | 38 | ||
| 39 | That includes auth behavior: | ||
| 40 | |||
| 41 | - plugin-owned auth config | ||
| 42 | - plugin-owned OAuth flows and refresh logic | ||
| 43 | - core-owned secret material services | ||
| 44 | |||
| 39 | ## What a new adapter should not do | 45 | ## What a new adapter should not do |
| 40 | 46 | ||
| 41 | Avoid these patterns: | 47 | Avoid these patterns: |
| @@ -54,7 +60,8 @@ A good new adapter should make the core system feel boring. · 12 unmodified lines | |||
| 54 | That means: | 60 | That means: |
| 55 | 61 | ||
| 56 | - persistence still looks like recipe/revision/operations | 62 | - persistence still looks like recipe/revision/operations |
| 57 | - auth still flows through the same credential/session primitives | 63 | - auth still uses the same secret material and private-state plumbing |
| 64 | - auth behavior itself stays plugin-owned | ||
| 58 | - discovery and inspection still work through the same catalog ideas | 65 | - discovery and inspection still work through the same catalog ideas |
| 59 | - execution still delegates through the same tool and adapter boundaries | 66 | - execution still delegates through the same tool and adapter boundaries |
| 60 | 67 | ||
| @@ -69,7 +76,7 @@ The current architecture is well-suited to future additions like: · 8 unmodified lines | |||
| 69 | - snippet bundles | 76 | - snippet bundles |
| 70 | - richer auth material providers | 77 | - richer auth material providers |
| 71 | 78 | ||
| 72 | Those should mostly be adapter and auth-layer work, not another persistence redesign. | 79 | Those should mostly be adapter and secret-material-layer work, not another persistence redesign. |
| 73 | 80 | ||
| 74 | ## Why this is the best setup | 81 | ## Why this is the best setup |
| 75 | 82 | ||
| @@ -80,4 +87,4 @@ It is trying to keep the codebase in a state where: · 4 unmodified lines | |||
| 80 | - new support lands as extensions | 87 | - new support lands as extensions |
| 81 | - source support does not require philosophy rewrites every few weeks | 88 | - source support does not require philosophy rewrites every few weeks |
| 82 | 89 | ||
| 83 | That is the real reason the system is shaped around adapters, recipes, revisions, operations, and actor-scoped credentials. | 90 | That is the real reason the system is shaped around adapters, recipes, revisions, operations, and actor-scoped private state. |
| @@ -9,7 +9,7 @@ MCP shows up in executor in two different ways. · 8 unmodified lines | |||
| 9 | 9 | ||
| 10 | This is the source side of MCP. | 10 | This is the source side of MCP. |
| 11 | 11 | ||
| 12 | When a user connects an MCP endpoint, executor treats it as an adapter with MCP-specific connection, discovery, and invocation behavior. The control plane persists the binding and credential state, while MCP-specific execution behavior lives behind the adapter and codemode layers. | 12 | When a user connects an MCP endpoint, executor treats it as an adapter with MCP-specific connection, discovery, invocation, and OAuth behavior. The control plane persists the binding and secret material state, while MCP-specific auth and execution behavior lives behind the adapter and codemode layers. |
| 13 | 13 | ||
| 14 | This is why MCP is modeled as its own adapter instead of being flattened into a generic HTTP execution model. | 14 | This is why MCP is modeled as its own adapter instead of being flattened into a generic HTTP execution model. |
| 15 | 15 | ||
| @@ -39,7 +39,7 @@ Keeping that distinction clear avoids collapsing very different responsibilities · 23 unmodified lines | |||
| 39 | 39 | ||
| 40 | Broadly: | 40 | Broadly: |
| 41 | 41 | ||
| 42 | - control plane owns workspace state, bindings, credentials, and execution lifecycle | 42 | - control plane owns workspace state, bindings, secret material, and execution lifecycle |
| 43 | - `packages/hosts/mcp` owns the local MCP server surface | 43 | - `packages/hosts/mcp` owns the local MCP server surface |
| 44 | - `packages/platform/server` mounts the `/mcp` handler | 44 | - `packages/platform/server` mounts the `/mcp` handler |
| 45 | 45 | ||
| @@ -7,7 +7,7 @@ Executor is built around one central idea: · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | - turn connected systems into a durable, discoverable tool catalog | 8 | - turn connected systems into a durable, discoverable tool catalog |
| 9 | - run TypeScript against that catalog inside a controlled runtime | 9 | - run TypeScript against that catalog inside a controlled runtime |
| 10 | - keep connection state, auth, execution, and inspection in one local control plane | 10 | - keep connection state, secret material, execution, and inspection in one local control plane |
| 11 | 11 | ||
| 12 | ## One-line architecture | 12 | ## One-line architecture |
| 13 | 13 | ||
| @@ -16,7 +16,7 @@ Executor is a local control plane that: · 2 unmodified lines | |||
| 16 | 1. stores connected integrations as sources | 16 | 1. stores connected integrations as sources |
| 17 | 2. materializes those sources into callable tools | 17 | 2. materializes those sources into callable tools |
| 18 | 3. exposes those tools to the runtime, the UI, and MCP clients | 18 | 3. exposes those tools to the runtime, the UI, and MCP clients |
| 19 | 4. resolves auth and policy at invocation time | 19 | 4. resolves secret material and policy at invocation time |
| 20 | 20 | ||
| 21 | ## Major packages | 21 | ## Major packages |
| 22 | 22 | ||
| @@ -48,7 +48,7 @@ The product core. · 25 unmodified lines | |||
| 48 | Responsibilities: | 48 | Responsibilities: |
| 49 | 49 | ||
| 50 | - source connection and lifecycle | 50 | - source connection and lifecycle |
| 51 | - auth and credential state | 51 | - secret material and private auth state |
| 52 | - persistence | 52 | - persistence |
| 53 | - tool discovery and inspection | 53 | - tool discovery and inspection |
| 54 | - execution management | 54 | - execution management |
| @@ -76,7 +76,7 @@ That is why executor has strong internal concepts like: · 21 unmodified lines | |||
| 76 | - recipes and revisions | 76 | - recipes and revisions |
| 77 | - materialized operations | 77 | - materialized operations |
| 78 | - schema bundles | 78 | - schema bundles |
| 79 | - actor-scoped credentials | 79 | - actor-scoped secret material |
| 80 | 80 | ||
| 81 | Those are not UI concepts. They are the durable internal model that lets executor support multiple integration styles without rewriting the core every time. | 81 | Those are not UI concepts. They are the durable internal model that lets executor support multiple integration styles without rewriting the core every time. |
| 82 | 82 | ||
| @@ -683,6 +683,7 @@ · 682 unmodified lines | |||
| 683 | "name": "@executor/plugin-openapi-shared", | 683 | "name": "@executor/plugin-openapi-shared", |
| 684 | "version": "0.0.0", | 684 | "version": "0.0.0", |
| 685 | "dependencies": { | 685 | "dependencies": { |
| 686 | "@executor/platform-sdk": "workspace:*", | ||
| 686 | "@executor/source-core": "workspace:*", | 687 | "@executor/source-core": "workspace:*", |
| 687 | "effect": "catalog:", | 688 | "effect": "catalog:", |
| 688 | "yaml": "^2.8.1", | 689 | "yaml": "^2.8.1", |
| @@ -99,7 +99,6 @@ export * from "./execution/live"; · 98 unmodified lines | |||
| 99 | export * from "./catalog/schema-type-signature"; | 99 | export * from "./catalog/schema-type-signature"; |
| 100 | export * from "./catalog/source/runtime"; | 100 | export * from "./catalog/source/runtime"; |
| 101 | export * from "./catalog/source/sync"; | 101 | export * from "./catalog/source/sync"; |
| 102 | export * from "./sources/source-credential-interactions"; | ||
| 103 | export * from "./sources/source-store"; | 102 | export * from "./sources/source-store"; |
| 104 | export * from "./executor-state-store"; | 103 | export * from "./executor-state-store"; |
| 105 | export * from "./execution/scope/environment"; | 104 | export * from "./execution/scope/environment"; |
| @@ -1,34 +0,0 @@ | |||
| 1 | import { | ||
| 2 | SecretRefSchema, | ||
| 3 | type SecretRef, | ||
| 4 | } from "#schema"; | ||
| 5 | import * as Schema from "effect/Schema"; | ||
| 6 | |||
| 7 | export const SourceCredentialSelectionContentSchema = Schema.Union( | ||
| 8 | Schema.Struct({ | ||
| 9 | authKind: Schema.Literal("none"), | ||
| 10 | }), | ||
| 11 | Schema.Struct({ | ||
| 12 | authKind: Schema.Literal("bearer"), | ||
| 13 | tokenRef: SecretRefSchema, | ||
| 14 | }), | ||
| 15 | ); | ||
| 16 | |||
| 17 | export type SourceCredentialSelectionContent = | ||
| 18 | typeof SourceCredentialSelectionContentSchema.Type; | ||
| 19 | |||
| 20 | export const decodeSourceCredentialSelectionContent = Schema.decodeUnknownSync( | ||
| 21 | SourceCredentialSelectionContentSchema, | ||
| 22 | ); | ||
| 23 | |||
| 24 | export const createSourceCredentialSelectionNoneContent = | ||
| 25 | (): SourceCredentialSelectionContent => ({ | ||
| 26 | authKind: "none", | ||
| 27 | }); | ||
| 28 | |||
| 29 | export const createSourceCredentialSelectionBearerContent = ( | ||
| 30 | tokenRef: SecretRef, | ||
| 31 | ): SourceCredentialSelectionContent => ({ | ||
| 32 | authKind: "bearer", | ||
| 33 | tokenRef, | ||
| 34 | }); | ||
| @@ -1,4 +1,3 @@ | |||
| 1 | import * as Data from "effect/Data"; | ||
| 2 | import * as Effect from "effect/Effect"; | 1 | import * as Effect from "effect/Effect"; |
| 3 | 2 | ||
| 4 | export const parseJsonValue = <T>(input: { | 3 | export const parseJsonValue = <T>(input: { |
| @@ -14,22 +13,3 @@ export const parseJsonValue = <T>(input: { · 9 unmodified lines | |||
| 14 | ? new Error(`Invalid ${input.label}: ${cause.message}`) | 13 | ? new Error(`Invalid ${input.label}: ${cause.message}`) |
| 15 | : new Error(`Invalid ${input.label}: ${String(cause)}`), | 14 | : new Error(`Invalid ${input.label}: ${String(cause)}`), |
| 16 | }); | 15 | }); |
| 17 | |||
| 18 | export class SourceCredentialRequiredError extends Data.TaggedError( | ||
| 19 | "SourceCredentialRequiredError", | ||
| 20 | )<{ | ||
| 21 | readonly slot: "runtime" | "import"; | ||
| 22 | readonly message: string; | ||
| 23 | }> { | ||
| 24 | constructor( | ||
| 25 | slot: "runtime" | "import", | ||
| 26 | message: string, | ||
| 27 | ) { | ||
| 28 | super({ slot, message }); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | export const isSourceCredentialRequiredError = ( | ||
| 33 | error: unknown, | ||
| 34 | ): error is SourceCredentialRequiredError => | ||
| 35 | error instanceof SourceCredentialRequiredError; | ||
| @@ -10,7 +10,6 @@ import { · 9 unmodified lines | |||
| 10 | import { | 10 | import { |
| 11 | createCatalogImportMetadata, | 11 | createCatalogImportMetadata, |
| 12 | createSourceCatalogSyncResult, | 12 | createSourceCatalogSyncResult, |
| 13 | SourceCredentialRequiredError, | ||
| 14 | } from "@executor/source-core"; | 13 | } from "@executor/source-core"; |
| 15 | import type { Source } from "@executor/platform-sdk/schema"; | 14 | import type { Source } from "@executor/platform-sdk/schema"; |
| 16 | import type { | 15 | import type { |
| @@ -294,8 +293,7 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 277 unmodified lines | |||
| 294 | signal: AbortSignal.timeout(15_000), | 293 | signal: AbortSignal.timeout(15_000), |
| 295 | }); | 294 | }); |
| 296 | if (response.status === 401 || response.status === 403) { | 295 | if (response.status === 401 || response.status === 403) { |
| 297 | throw new SourceCredentialRequiredError( | 296 | throw new Error( |
| 298 | "import", | ||
| 299 | `Google Discovery fetch requires credentials (status ${response.status})`, | 297 | `Google Discovery fetch requires credentials (status ${response.status})`, |
| 300 | ); | 298 | ); |
| 301 | } | 299 | } |
| @@ -9,7 +9,6 @@ import * as Schema from "effect/Schema"; · 8 unmodified lines | |||
| 9 | import { | 9 | import { |
| 10 | createCatalogImportMetadata, | 10 | createCatalogImportMetadata, |
| 11 | createSourceCatalogSyncResult, | 11 | createSourceCatalogSyncResult, |
| 12 | SourceCredentialRequiredError, | ||
| 13 | } from "@executor/source-core"; | 12 | } from "@executor/source-core"; |
| 14 | import type { Source } from "@executor/platform-sdk/schema"; | 13 | import type { Source } from "@executor/platform-sdk/schema"; |
| 15 | import type { | 14 | import type { |
| @@ -200,8 +199,7 @@ const fetchGraphqlIntrospectionDocumentWithHeaders = (input: { · 184 unmodified lines | |||
| 200 | 199 | ||
| 201 | if (!response.ok) { | 200 | if (!response.ok) { |
| 202 | if (response.status === 401 || response.status === 403) { | 201 | if (response.status === 401 || response.status === 403) { |
| 203 | throw new SourceCredentialRequiredError( | 202 | throw new Error( |
| 204 | "import", | ||
| 205 | `GraphQL introspection requires credentials (status ${response.status})`, | 203 | `GraphQL introspection requires credentials (status ${response.status})`, |
| 206 | ); | 204 | ); |
| 207 | } | 205 | } |
| @@ -38,7 +38,7 @@ import { · 37 unmodified lines | |||
| 38 | type SourceToolExplorerSearch, | 38 | type SourceToolExplorerSearch, |
| 39 | useSourcePluginNavigation, | 39 | useSourcePluginNavigation, |
| 40 | useSourcePluginSearch, | 40 | useSourcePluginSearch, |
| 41 | } from "@executor/react/source-plugins"; | 41 | } from "@executor/react/plugins"; |
| 42 | import { | 42 | import { |
| 43 | openApiHttpApiExtension, | 43 | openApiHttpApiExtension, |
| 44 | } from "@executor/plugin-openapi-http"; | 44 | } from "@executor/plugin-openapi-http"; |
| @@ -169,6 +169,31 @@ const inputFromConfig = ( · 124 unmodified lines | |||
| 169 | auth: config.auth, | 169 | auth: config.auth, |
| 170 | }); | 170 | }); |
| 171 | 171 | ||
| 172 | const secretValue = (input: OpenApiConnectInput["auth"]): string => | ||
| 173 | input.kind === "bearer" | ||
| 174 | ? JSON.stringify(input.tokenSecretRef) | ||
| 175 | : ""; | ||
| 176 | |||
| 177 | const authFromSecretValue = ( | ||
| 178 | authKind: OpenApiConnectInput["auth"]["kind"], | ||
| 179 | value: string, | ||
| 180 | ): OpenApiConnectInput["auth"] => { | ||
| 181 | if (authKind === "none") { | ||
| 182 | return { | ||
| 183 | kind: "none", | ||
| 184 | }; | ||
| 185 | } | ||
| 186 | |||
| 187 | if (!value) { | ||
| 188 | throw new Error("Select a secret for bearer auth."); | ||
| 189 | } | ||
| 190 | |||
| 191 | return { | ||
| 192 | kind: "bearer", | ||
| 193 | tokenSecretRef: JSON.parse(value) as OpenApiConnectInput["auth"] & { tokenSecretRef: never }["tokenSecretRef"], | ||
| 194 | }; | ||
| 195 | }; | ||
| 196 | |||
| 172 | const useAvailableSecrets = ( | 197 | const useAvailableSecrets = ( |
| 173 | openApiHttpClient: ReturnType<typeof getOpenApiHttpClient>, | 198 | openApiHttpClient: ReturnType<typeof getOpenApiHttpClient>, |
| 174 | ) => { | 199 | ) => { |
| @@ -205,9 +230,7 @@ function OpenApiSourceForm(props: { · 30 unmodified lines | |||
| 205 | props.initialValue.auth.kind, | 230 | props.initialValue.auth.kind, |
| 206 | ); | 231 | ); |
| 207 | const [tokenSecretRef, setTokenSecretRef] = useState( | 232 | const [tokenSecretRef, setTokenSecretRef] = useState( |
| 208 | props.initialValue.auth.kind === "bearer" | 233 | secretValue(props.initialValue.auth), |
| 209 | ? props.initialValue.auth.tokenSecretRef | ||
| 210 | : "", | ||
| 211 | ); | 234 | ); |
| 212 | const [error, setError] = useState<string | null>(null); | 235 | const [error, setError] = useState<string | null>(null); |
| 213 | const [preview, setPreview] = useState<OpenApiPreviewResponse | null>(null); | 236 | const [preview, setPreview] = useState<OpenApiPreviewResponse | null>(null); |
| @@ -309,19 +332,12 @@ function OpenApiSourceForm(props: { · 95 unmodified lines | |||
| 309 | } | 332 | } |
| 310 | 333 | ||
| 311 | try { | 334 | try { |
| 335 | const auth = authFromSecretValue(authKind, tokenSecretRef); | ||
| 312 | await submitMutation.mutateAsync({ | 336 | await submitMutation.mutateAsync({ |
| 313 | name: trimmedName, | 337 | name: trimmedName, |
| 314 | specUrl: trimmedSpecUrl, | 338 | specUrl: trimmedSpecUrl, |
| 315 | baseUrl: trimmedBaseUrl || null, | 339 | baseUrl: trimmedBaseUrl || null, |
| 316 | auth: | 340 | auth, |
| 317 | authKind === "bearer" | ||
| 318 | ? { | ||
| 319 | kind: "bearer", | ||
| 320 | tokenSecretRef: tokenSecretRef.trim(), | ||
| 321 | } | ||
| 322 | : { | ||
| 323 | kind: "none", | ||
| 324 | }, | ||
| 325 | }); | 341 | }); |
| 326 | } catch (cause) { | 342 | } catch (cause) { |
| 327 | setError(cause instanceof Error ? cause.message : "Failed saving source."); | 343 | setError(cause instanceof Error ? cause.message : "Failed saving source."); |
| @@ -398,7 +414,13 @@ function OpenApiSourceForm(props: { · 70 unmodified lines | |||
| 398 | > | 414 | > |
| 399 | <option value="">Select a secret</option> | 415 | <option value="">Select a secret</option> |
| 400 | {availableSecrets.map((secret) => ( | 416 | {availableSecrets.map((secret) => ( |
| 401 | <option key={secret.id} value={secret.id}> | 417 | <option |
| 418 | key={`${secret.providerId}:${secret.id}`} | ||
| 419 | value={JSON.stringify({ | ||
| 420 | providerId: secret.providerId, | ||
| 421 | handle: secret.id, | ||
| 422 | })} | ||
| 423 | > | ||
| 402 | {secret.name || secret.id} | 424 | {secret.name || secret.id} |
| 403 | </option> | 425 | </option> |
| 404 | ))} | 426 | ))} |
| @@ -13,6 +13,9 @@ import type { · 12 unmodified lines | |||
| 13 | ExecutorSourceConnector, | 13 | ExecutorSourceConnector, |
| 14 | SourcePluginRuntime, | 14 | SourcePluginRuntime, |
| 15 | } from "@executor/platform-sdk/plugins"; | 15 | } from "@executor/platform-sdk/plugins"; |
| 16 | import { | ||
| 17 | SecretMaterialResolverService, | ||
| 18 | } from "@executor/platform-sdk/runtime"; | ||
| 16 | import { | 19 | import { |
| 17 | OpenApiConnectionAuthSchema, | 20 | OpenApiConnectionAuthSchema, |
| 18 | deriveOpenApiNamespace, | 21 | deriveOpenApiNamespace, |
| @@ -64,12 +67,6 @@ export type OpenApiSourceStorage = { · 45 unmodified lines | |||
| 64 | }) => Effect.Effect<void, Error, never>; | 67 | }) => Effect.Effect<void, Error, never>; |
| 65 | }; | 68 | }; |
| 66 | 69 | ||
| 67 | export type OpenApiSecrets = { | ||
| 68 | resolve: (input: { | ||
| 69 | ref: string; | ||
| 70 | }) => Effect.Effect<string, Error, never>; | ||
| 71 | }; | ||
| 72 | |||
| 73 | export type OpenApiSdk = { | 70 | export type OpenApiSdk = { |
| 74 | previewDocument: ( | 71 | previewDocument: ( |
| 75 | input: OpenApiPreviewRequest, | 72 | input: OpenApiPreviewRequest, |
| @@ -93,7 +90,6 @@ export type OpenApiSdk = { · 17 unmodified lines | |||
| 93 | 90 | ||
| 94 | export type OpenApiSdkPluginOptions = { | 91 | export type OpenApiSdkPluginOptions = { |
| 95 | storage: OpenApiSourceStorage; | 92 | storage: OpenApiSourceStorage; |
| 96 | secrets?: OpenApiSecrets; | ||
| 97 | }; | 93 | }; |
| 98 | 94 | ||
| 99 | const OpenApiExecutorAddInputSchema = Schema.Struct({ | 95 | const OpenApiExecutorAddInputSchema = Schema.Struct({ |
| @@ -414,22 +410,18 @@ const decodeResponseBody = async (response: Response): Promise<unknown> => { · 314 unmodified lines | |||
| 414 | }; | 410 | }; |
| 415 | 411 | ||
| 416 | const resolveBearerToken = ( | 412 | const resolveBearerToken = ( |
| 417 | options: OpenApiSdkPluginOptions, | ||
| 418 | stored: OpenApiStoredSourceData, | 413 | stored: OpenApiStoredSourceData, |
| 419 | ): Effect.Effect<string | null, Error, never> => { | 414 | ): Effect.Effect<string | null, Error, any> => { |
| 420 | if (stored.auth.kind === "none") { | 415 | const { auth } = stored; |
| 416 | if (auth.kind === "none") { | ||
| 421 | return Effect.succeed(null); | 417 | return Effect.succeed(null); |
| 422 | } | 418 | } |
| 423 | 419 | ||
| 424 | if (!options.secrets) { | 420 | return Effect.flatMap(SecretMaterialResolverService, (resolveSecretMaterial) => |
| 425 | return Effect.fail( | 421 | resolveSecretMaterial({ |
| 426 | new Error("OpenAPI bearer auth is configured, but no secret resolver is available."), | 422 | ref: auth.tokenSecretRef, |
| 427 | ); | 423 | }).pipe(Effect.map((token) => token.trim())) |
| 428 | } | 424 | ); |
| 429 | |||
| 430 | return options.secrets.resolve({ | ||
| 431 | ref: stored.auth.tokenSecretRef, | ||
| 432 | }).pipe(Effect.map((token) => token.trim())); | ||
| 433 | }; | 425 | }; |
| 434 | 426 | ||
| 435 | const openApiDocumentHeaders = (input: { | 427 | const openApiDocumentHeaders = (input: { |
| @@ -550,7 +542,7 @@ const createOpenApiSourceRuntime = ( · 114 unmodified lines | |||
| 550 | }); | 542 | }); |
| 551 | } | 543 | } |
| 552 | 544 | ||
| 553 | const bearerToken = yield* resolveBearerToken(options, stored); | 545 | const bearerToken = yield* resolveBearerToken(stored); |
| 554 | const fetched = yield* fetchOpenApiDocument({ | 546 | const fetched = yield* fetchOpenApiDocument({ |
| 555 | stored, | 547 | stored, |
| 556 | bearerToken, | 548 | bearerToken, |
| @@ -686,7 +678,7 @@ const createOpenApiSourceRuntime = ( · 129 unmodified lines | |||
| 686 | } | 678 | } |
| 687 | } | 679 | } |
| 688 | 680 | ||
| 689 | const bearerToken = yield* resolveBearerToken(options, stored); | 681 | const bearerToken = yield* resolveBearerToken(stored); |
| 690 | if (bearerToken && bearerToken.length > 0) { | 682 | if (bearerToken && bearerToken.length > 0) { |
| 691 | headers.authorization = `Bearer ${bearerToken}`; | 683 | headers.authorization = `Bearer ${bearerToken}`; |
| 692 | } | 684 | } |
| @@ -3,8 +3,9 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines | |||
| 3 | import YAML from "yaml"; | 3 | import YAML from "yaml"; |
| 4 | 4 | ||
| 5 | import { | 5 | import { |
| 6 | SecretRefSchema, | ||
| 6 | StringMapSchema, | 7 | StringMapSchema, |
| 7 | } from "@executor/source-core"; | 8 | } from "@executor/platform-sdk/schema"; |
| 8 | 9 | ||
| 9 | export const OpenApiConnectionAuthSchema = Schema.Union( | 10 | export const OpenApiConnectionAuthSchema = Schema.Union( |
| 10 | Schema.Struct({ | 11 | Schema.Struct({ |
| @@ -12,7 +13,7 @@ export const OpenApiConnectionAuthSchema = Schema.Union( · 1 unmodified line | |||
| 12 | }), | 13 | }), |
| 13 | Schema.Struct({ | 14 | Schema.Struct({ |
| 14 | kind: Schema.Literal("bearer"), | 15 | kind: Schema.Literal("bearer"), |
| 15 | tokenSecretRef: Schema.String, | 16 | tokenSecretRef: SecretRefSchema, |
| 16 | }), | 17 | }), |
| 17 | ); | 18 | ); |
| 18 | 19 | ||
| @@ -12,6 +12,7 @@ · 11 unmodified lines | |||
| 12 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 12 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 13 | }, | 13 | }, |
| 14 | "dependencies": { | 14 | "dependencies": { |
| 15 | "@executor/platform-sdk": "workspace:*", | ||
| 15 | "@executor/source-core": "workspace:*", | 16 | "@executor/source-core": "workspace:*", |
| 16 | "effect": "catalog:", | 17 | "effect": "catalog:", |
| 17 | "yaml": "^2.8.1" | 18 | "yaml": "^2.8.1" |