Unified · Split
| @@ -1,6 +1,6 @@ | |||
| 1 | /// <reference types="next" /> | 1 | /// <reference types="next" /> |
| 2 | /// <reference types="next/image-types/global" /> | 2 | /// <reference types="next/image-types/global" /> |
| 3 | import "./.next/types/routes.d.ts"; | 3 | import "./.next/dev/types/routes.d.ts"; |
| 4 | 4 | ||
| 5 | // NOTE: This file should not be edited | 5 | // NOTE: This file should not be edited |
| 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. | 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. |
| @@ -1,9 +1,7 @@ | |||
| 1 | import { parse as parseYaml } from "yaml"; | 1 | import { parse as parseYaml } from "yaml"; |
| 2 | import { z } from "zod"; | 2 | import { z } from "zod"; |
| 3 | import { unstable_cache } from "next/cache"; | ||
| 4 | import { prepareOpenApiSpec } from "@executor/core/openapi-prepare"; | 3 | import { prepareOpenApiSpec } from "@executor/core/openapi-prepare"; |
| 5 | 4 | ||
| 6 | const GENERATED_SPEC_REVALIDATE_SECONDS = 60 * 60; | ||
| 7 | const HEADER_FETCH_TIMEOUT_MS = 12_000; | 5 | const HEADER_FETCH_TIMEOUT_MS = 12_000; |
| 8 | 6 | ||
| 9 | const querySchema = z.object({ | 7 | const querySchema = z.object({ |
| @@ -26,17 +24,6 @@ const blockedForwardedHeaderNames = new Set([ · 16 unmodified lines | |||
| 26 | "referer", | 24 | "referer", |
| 27 | ]); | 25 | ]); |
| 28 | 26 | ||
| 29 | const generatePreparedSpecCached = unstable_cache( | ||
| 30 | async (specUrl: string, includeDts: boolean) => { | ||
| 31 | return await prepareOpenApiSpec(specUrl, "openapi", { | ||
| 32 | includeDts, | ||
| 33 | profile: "inventory", | ||
| 34 | }); | ||
| 35 | }, | ||
| 36 | ["openapi-generate-v1"], | ||
| 37 | { revalidate: GENERATED_SPEC_REVALIDATE_SECONDS }, | ||
| 38 | ); | ||
| 39 | |||
| 40 | function jsonResponse( | 27 | function jsonResponse( |
| 41 | payload: unknown, | 28 | payload: unknown, |
| 42 | status: number, | 29 | status: number, |
| @@ -172,7 +159,10 @@ export async function GET(request: Request): Promise<Response> { · 129 unmodified lines | |||
| 172 | const forwardedHeaders = sanitizeForwardHeaders(parseHeadersQueryValue(parsed.data.headers)); | 159 | const forwardedHeaders = sanitizeForwardHeaders(parseHeadersQueryValue(parsed.data.headers)); |
| 173 | 160 | ||
| 174 | const prepared = Object.keys(forwardedHeaders).length === 0 | 161 | const prepared = Object.keys(forwardedHeaders).length === 0 |
| 175 | ? await generatePreparedSpecCached(parsed.data.specUrl, includeDts) | 162 | ? await prepareOpenApiSpec(parsed.data.specUrl, sourceName, { |
| 163 | includeDts, | ||
| 164 | profile: "inventory", | ||
| 165 | }) | ||
| 176 | : await prepareWithForwardedHeaders( | 166 | : await prepareWithForwardedHeaders( |
| 177 | parsed.data.specUrl, | 167 | parsed.data.specUrl, |
| 178 | sourceName, | 168 | sourceName, |
| @@ -144,6 +144,7 @@ export function AddSourceDialog({ · 143 unmodified lines | |||
| 144 | endpoint: form.endpoint.trim(), | 144 | endpoint: form.endpoint.trim(), |
| 145 | baseUrl: form.baseUrl, | 145 | baseUrl: form.baseUrl, |
| 146 | auth: form.buildAuthConfig(), | 146 | auth: form.buildAuthConfig(), |
| 147 | useCredentialedFetch: form.useCredentialedFetch, | ||
| 147 | mcpTransport: form.mcpTransport, | 148 | mcpTransport: form.mcpTransport, |
| 148 | accountId: context.accountId, | 149 | accountId: context.accountId, |
| 149 | }); | 150 | }); |
| @@ -227,6 +228,7 @@ export function AddSourceDialog({ · 77 unmodified lines | |||
| 227 | authType: form.authType, | 228 | authType: form.authType, |
| 228 | authScope: form.authScope, | 229 | authScope: form.authScope, |
| 229 | apiKeyHeader: form.apiKeyHeader, | 230 | apiKeyHeader: form.apiKeyHeader, |
| 231 | useCredentialedFetch: form.useCredentialedFetch, | ||
| 230 | existingScopedCredential: form.existingScopedCredential, | 232 | existingScopedCredential: form.existingScopedCredential, |
| 231 | buildAuthConfig: form.buildAuthConfig, | 233 | buildAuthConfig: form.buildAuthConfig, |
| 232 | hasCredentialInput: form.hasCredentialInput, | 234 | hasCredentialInput: form.hasCredentialInput, |
| @@ -35,6 +35,7 @@ type SaveFormSnapshot = { · 34 unmodified lines | |||
| 35 | authType: Exclude<SourceAuthType, "mixed">; | 35 | authType: Exclude<SourceAuthType, "mixed">; |
| 36 | authScope: CredentialScope; | 36 | authScope: CredentialScope; |
| 37 | apiKeyHeader: string; | 37 | apiKeyHeader: string; |
| 38 | useCredentialedFetch: boolean; | ||
| 38 | existingScopedCredential: CredentialRecord | null; | 39 | existingScopedCredential: CredentialRecord | null; |
| 39 | buildAuthConfig: () => Record<string, unknown> | undefined; | 40 | buildAuthConfig: () => Record<string, unknown> | undefined; |
| 40 | hasCredentialInput: () => boolean; | 41 | hasCredentialInput: () => boolean; |
| @@ -120,6 +121,7 @@ export async function saveSourceWithCredentials({ · 79 unmodified lines | |||
| 120 | endpoint: form.endpoint.trim(), | 121 | endpoint: form.endpoint.trim(), |
| 121 | baseUrl: form.baseUrl, | 122 | baseUrl: form.baseUrl, |
| 122 | auth: authConfig, | 123 | auth: authConfig, |
| 124 | useCredentialedFetch: form.useCredentialedFetch, | ||
| 123 | mcpTransport: form.mcpTransport, | 125 | mcpTransport: form.mcpTransport, |
| 124 | accountId: context.accountId, | 126 | accountId: context.accountId, |
| 125 | }); | 127 | }); |
| @@ -172,19 +172,17 @@ export function SourceAuthPanel({ · 171 unmodified lines | |||
| 172 | </p> | 172 | </p> |
| 173 | ) : null} | 173 | ) : null} |
| 174 | 174 | ||
| 175 | {sourceType === "openapi" ? ( | 175 | <label className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2"> |
| 176 | <label className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2"> | 176 | <input |
| 177 | <input | 177 | type="checkbox" |
| 178 | type="checkbox" | 178 | checked={useCredentialedFetch} |
| 179 | checked={useCredentialedFetch} | 179 | onChange={(event) => onUseCredentialedFetchChange(event.target.checked)} |
| 180 | onChange={(event) => onUseCredentialedFetchChange(event.target.checked)} | 180 | className="h-3.5 w-3.5" |
| 181 | className="h-3.5 w-3.5" | 181 | /> |
| 182 | /> | 182 | <span className="text-[11px] text-muted-foreground"> |
| 183 | <span className="text-[11px] text-muted-foreground"> | 183 | Use credentials while fetching source metadata |
| 184 | Use credentials while detecting/fetching spec (off by default for shared cache speed) | 184 | </span> |
| 185 | </span> | 185 | </label> |
| 186 | </label> | ||
| 187 | ) : null} | ||
| 188 | 186 | ||
| 189 | {sourceType === "mcp" && mcpOAuthStatus === "checking" ? ( | 187 | {sourceType === "mcp" && mcpOAuthStatus === "checking" ? ( |
| 190 | <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground"> | 188 | <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground"> |
| @@ -42,6 +42,7 @@ export function createCustomSourceConfig({ · 41 unmodified lines | |||
| 42 | endpoint, | 42 | endpoint, |
| 43 | baseUrl, | 43 | baseUrl, |
| 44 | auth, | 44 | auth, |
| 45 | useCredentialedFetch, | ||
| 45 | mcpTransport, | 46 | mcpTransport, |
| 46 | accountId, | 47 | accountId, |
| 47 | }: { | 48 | }: { |
| @@ -49,6 +50,7 @@ export function createCustomSourceConfig({ · 1 unmodified line | |||
| 49 | endpoint: string; | 50 | endpoint: string; |
| 50 | baseUrl: string; | 51 | baseUrl: string; |
| 51 | auth?: Record<string, unknown>; | 52 | auth?: Record<string, unknown>; |
| 53 | useCredentialedFetch: boolean; | ||
| 52 | mcpTransport: "auto" | "streamable-http" | "sse"; | 54 | mcpTransport: "auto" | "streamable-http" | "sse"; |
| 53 | accountId?: string; | 55 | accountId?: string; |
| 54 | }): Record<string, unknown> { | 56 | }): Record<string, unknown> { |
| @@ -57,6 +59,7 @@ export function createCustomSourceConfig({ · 2 unmodified lines | |||
| 57 | if (type === "mcp") { | 59 | if (type === "mcp") { |
| 58 | return { | 60 | return { |
| 59 | url: normalizedEndpoint, | 61 | url: normalizedEndpoint, |
| 62 | useCredentialedFetch, | ||
| 60 | ...(auth ? { auth } : {}), | 63 | ...(auth ? { auth } : {}), |
| 61 | ...(mcpTransport !== "auto" ? { transport: mcpTransport } : {}), | 64 | ...(mcpTransport !== "auto" ? { transport: mcpTransport } : {}), |
| 62 | ...(accountId | 65 | ...(accountId |
| @@ -68,6 +71,7 @@ export function createCustomSourceConfig({ · 5 unmodified lines | |||
| 68 | if (type === "graphql") { | 71 | if (type === "graphql") { |
| 69 | return { | 72 | return { |
| 70 | endpoint: normalizedEndpoint, | 73 | endpoint: normalizedEndpoint, |
| 74 | useCredentialedFetch, | ||
| 71 | ...(auth ? { auth } : {}), | 75 | ...(auth ? { auth } : {}), |
| 72 | }; | 76 | }; |
| 73 | } | 77 | } |
| @@ -75,6 +79,7 @@ export function createCustomSourceConfig({ · 1 unmodified line | |||
| 75 | return { | 79 | return { |
| 76 | spec: normalizedEndpoint, | 80 | spec: normalizedEndpoint, |
| 77 | specUrl: normalizedEndpoint, | 81 | specUrl: normalizedEndpoint, |
| 82 | useCredentialedFetch, | ||
| 78 | ...(baseUrl ? { baseUrl } : {}), | 83 | ...(baseUrl ? { baseUrl } : {}), |
| 79 | ...(auth ? { auth } : {}), | 84 | ...(auth ? { auth } : {}), |
| 80 | }; | 85 | }; |
| @@ -178,7 +178,7 @@ export function sourceToFormValues(source: ToolSourceRecord): SourceFormValues { · 177 unmodified lines | |||
| 178 | apiKeyValue: "", | 178 | apiKeyValue: "", |
| 179 | basicUsername: "", | 179 | basicUsername: "", |
| 180 | basicPassword: "", | 180 | basicPassword: "", |
| 181 | useCredentialedFetch: false, | 181 | useCredentialedFetch: source.config.useCredentialedFetch === true, |
| 182 | }; | 182 | }; |
| 183 | } | 183 | } |
| 184 | 184 | ||
| @@ -182,6 +182,7 @@ export function SourceFormPanel({ · 181 unmodified lines | |||
| 182 | endpoint: form.endpoint.trim(), | 182 | endpoint: form.endpoint.trim(), |
| 183 | baseUrl: form.baseUrl, | 183 | baseUrl: form.baseUrl, |
| 184 | auth: form.buildAuthConfig(), | 184 | auth: form.buildAuthConfig(), |
| 185 | useCredentialedFetch: form.useCredentialedFetch, | ||
| 185 | mcpTransport: form.mcpTransport, | 186 | mcpTransport: form.mcpTransport, |
| 186 | accountId: context.accountId, | 187 | accountId: context.accountId, |
| 187 | }); | 188 | }); |
| @@ -264,6 +265,7 @@ export function SourceFormPanel({ · 76 unmodified lines | |||
| 264 | authType: form.authType, | 265 | authType: form.authType, |
| 265 | authScope: form.authScope, | 266 | authScope: form.authScope, |
| 266 | apiKeyHeader: form.apiKeyHeader, | 267 | apiKeyHeader: form.apiKeyHeader, |
| 268 | useCredentialedFetch: form.useCredentialedFetch, | ||
| 267 | existingScopedCredential: form.existingScopedCredential, | 269 | existingScopedCredential: form.existingScopedCredential, |
| 268 | buildAuthConfig: form.buildAuthConfig, | 270 | buildAuthConfig: form.buildAuthConfig, |
| 269 | hasCredentialInput: form.hasCredentialInput, | 271 | hasCredentialInput: form.hasCredentialInput, |
| @@ -6,6 +6,7 @@ export interface McpToolSourceConfig { · 5 unmodified lines | |||
| 6 | sourceId?: string; | 6 | sourceId?: string; |
| 7 | sourceKey?: string; | 7 | sourceKey?: string; |
| 8 | url: string; | 8 | url: string; |
| 9 | useCredentialedFetch?: boolean; | ||
| 9 | auth?: OpenApiAuth; | 10 | auth?: OpenApiAuth; |
| 10 | discoveryHeaders?: Record<string, string>; | 11 | discoveryHeaders?: Record<string, string>; |
| 11 | transport?: "sse" | "streamable-http"; | 12 | transport?: "sse" | "streamable-http"; |
| @@ -26,6 +27,7 @@ export interface OpenApiToolSourceConfig { · 14 unmodified lines | |||
| 26 | sourceId?: string; | 27 | sourceId?: string; |
| 27 | sourceKey?: string; | 28 | sourceKey?: string; |
| 28 | spec: string | Record<string, unknown>; | 29 | spec: string | Record<string, unknown>; |
| 30 | useCredentialedFetch?: boolean; | ||
| 29 | collectionUrl?: string; | 31 | collectionUrl?: string; |
| 30 | postmanProxyUrl?: string; | 32 | postmanProxyUrl?: string; |
| 31 | baseUrl?: string; | 33 | baseUrl?: string; |
| @@ -41,6 +43,7 @@ export interface GraphqlToolSourceConfig { · 9 unmodified lines | |||
| 41 | sourceId?: string; | 43 | sourceId?: string; |
| 42 | sourceKey?: string; | 44 | sourceKey?: string; |
| 43 | endpoint: string; | 45 | endpoint: string; |
| 46 | useCredentialedFetch?: boolean; | ||
| 44 | schema?: Record<string, unknown>; | 47 | schema?: Record<string, unknown>; |
| 45 | auth?: OpenApiAuth; | 48 | auth?: OpenApiAuth; |
| 46 | defaultQueryApproval?: ToolApprovalMode; | 49 | defaultQueryApproval?: ToolApprovalMode; |
| @@ -392,6 +392,41 @@ export const resolveCredential = internalQuery({ · 391 unmodified lines | |||
| 392 | } | 392 | } |
| 393 | 393 | ||
| 394 | const organizationId = workspace.organizationId; | 394 | const organizationId = workspace.organizationId; |
| 395 | const sourceId = sourceIdFromSourceKey(args.sourceKey); | ||
| 396 | if (sourceId) { | ||
| 397 | const source = await ctx.db | ||
| 398 | .query("toolSources") | ||
| 399 | .withIndex("by_source_id", (q) => q.eq("sourceId", sourceId)) | ||
| 400 | .unique(); | ||
| 401 | |||
| 402 | if (source) { | ||
| 403 | if (source.organizationId !== organizationId) { | ||
| 404 | return null; | ||
| 405 | } | ||
| 406 | |||
| 407 | if (source.scopeType === "workspace" && source.workspaceId !== args.workspaceId) { | ||
| 408 | return null; | ||
| 409 | } | ||
| 410 | } | ||
| 411 | } | ||
| 412 | |||
| 413 | if (args.scopeType === "account") { | ||
| 414 | const accountId = args.accountId; | ||
| 415 | if (!accountId) { | ||
| 416 | return null; | ||
| 417 | } | ||
| 418 | |||
| 419 | const membership = await ctx.db | ||
| 420 | .query("organizationMembers") | ||
| 421 | .withIndex("by_org_account", (q) => | ||
| 422 | q.eq("organizationId", organizationId).eq("accountId", accountId), | ||
| 423 | ) | ||
| 424 | .unique(); | ||
| 425 | |||
| 426 | if (!membership || membership.status !== "active") { | ||
| 427 | return null; | ||
| 428 | } | ||
| 429 | } | ||
| 395 | 430 | ||
| 396 | const tryWorkspace = async () => { | 431 | const tryWorkspace = async () => { |
| 397 | return await ctx.db | 432 | return await ctx.db |
| @@ -13,17 +13,17 @@ export const TOOL_SOURCE_CONFIG_VERSION = 1; · 12 unmodified lines | |||
| 13 | 13 | ||
| 14 | export type NormalizedMcpToolSourceConfig = Pick< | 14 | export type NormalizedMcpToolSourceConfig = Pick< |
| 15 | McpToolSourceConfig, | 15 | McpToolSourceConfig, |
| 16 | "url" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides" | 16 | "url" | "useCredentialedFetch" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides" |
| 17 | >; | 17 | >; |
| 18 | 18 | ||
| 19 | export type NormalizedGraphqlToolSourceConfig = Pick< | 19 | export type NormalizedGraphqlToolSourceConfig = Pick< |
| 20 | GraphqlToolSourceConfig, | 20 | GraphqlToolSourceConfig, |
| 21 | "endpoint" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides" | 21 | "endpoint" | "useCredentialedFetch" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides" |
| 22 | >; | 22 | >; |
| 23 | 23 | ||
| 24 | export type NormalizedOpenApiToolSourceConfig = Pick< | 24 | export type NormalizedOpenApiToolSourceConfig = Pick< |
| 25 | OpenApiToolSourceConfig, | 25 | OpenApiToolSourceConfig, |
| 26 | "spec" | "collectionUrl" | "postmanProxyUrl" | "baseUrl" | "auth" | "defaultReadApproval" | "defaultWriteApproval" | "overrides" | 26 | "spec" | "useCredentialedFetch" | "collectionUrl" | "postmanProxyUrl" | "baseUrl" | "auth" | "defaultReadApproval" | "defaultWriteApproval" | "overrides" |
| 27 | >; | 27 | >; |
| 28 | 28 | ||
| 29 | export type NormalizedToolSourceConfig = | 29 | export type NormalizedToolSourceConfig = |
| @@ -315,6 +315,7 @@ export function normalizeToolSourceConfig( · 285 unmodified lines | |||
| 315 | 315 | ||
| 316 | return Result.ok({ | 316 | return Result.ok({ |
| 317 | url: urlResult.value, | 317 | url: urlResult.value, |
| 318 | useCredentialedFetch: config.useCredentialedFetch === true, | ||
| 318 | transport, | 319 | transport, |
| 319 | auth: authResult.value, | 320 | auth: authResult.value, |
| 320 | queryParams: queryParamsResult.value, | 321 | queryParams: queryParamsResult.value, |
| @@ -361,6 +362,7 @@ export function normalizeToolSourceConfig( · 40 unmodified lines | |||
| 361 | 362 | ||
| 362 | return Result.ok({ | 363 | return Result.ok({ |
| 363 | endpoint: endpointResult.value, | 364 | endpoint: endpointResult.value, |
| 365 | useCredentialedFetch: config.useCredentialedFetch === true, | ||
| 364 | schema: Object.keys(schema).length > 0 ? schema : undefined, | 366 | schema: Object.keys(schema).length > 0 ? schema : undefined, |
| 365 | auth: authResult.value, | 367 | auth: authResult.value, |
| 366 | defaultQueryApproval: defaultQueryApprovalResult.value, | 368 | defaultQueryApproval: defaultQueryApprovalResult.value, |
| @@ -402,6 +404,7 @@ export function normalizeToolSourceConfig( · 35 unmodified lines | |||
| 402 | 404 | ||
| 403 | return Result.ok({ | 405 | return Result.ok({ |
| 404 | spec: specResult.value, | 406 | spec: specResult.value, |
| 407 | useCredentialedFetch: config.useCredentialedFetch === true, | ||
| 405 | collectionUrl: optionalTrimmedString(config.collectionUrl), | 408 | collectionUrl: optionalTrimmedString(config.collectionUrl), |
| 406 | postmanProxyUrl: optionalTrimmedString(config.postmanProxyUrl), | 409 | postmanProxyUrl: optionalTrimmedString(config.postmanProxyUrl), |
| 407 | baseUrl: optionalTrimmedString(config.baseUrl), | 410 | baseUrl: optionalTrimmedString(config.baseUrl), |
| @@ -105,6 +105,7 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 104 unmodified lines | |||
| 105 | specUrl: string, | 105 | specUrl: string, |
| 106 | sourceName: string, | 106 | sourceName: string, |
| 107 | includeDts: boolean, | 107 | includeDts: boolean, |
| 108 | headers?: Record<string, string>, | ||
| 108 | ): Promise<PreparedOpenApiSpec | null> { | 109 | ): Promise<PreparedOpenApiSpec | null> { |
| 109 | const endpoint = resolveExternalGenerateEndpoint(); | 110 | const endpoint = resolveExternalGenerateEndpoint(); |
| 110 | if (!endpoint) { | 111 | if (!endpoint) { |
| @@ -119,6 +120,9 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 8 unmodified lines | |||
| 119 | url.searchParams.set("specUrl", specUrl); | 120 | url.searchParams.set("specUrl", specUrl); |
| 120 | url.searchParams.set("sourceName", sourceName); | 121 | url.searchParams.set("sourceName", sourceName); |
| 121 | url.searchParams.set("includeDts", includeDts ? "1" : "0"); | 122 | url.searchParams.set("includeDts", includeDts ? "1" : "0"); |
| 123 | if (headers && Object.keys(headers).length > 0) { | ||
| 124 | url.searchParams.set("headers", JSON.stringify(headers)); | ||
| 125 | } | ||
| 122 | 126 | ||
| 123 | const response = await fetch(url.toString(), { | 127 | const response = await fetch(url.toString(), { |
| 124 | method: "GET", | 128 | method: "GET", |
| @@ -353,6 +357,7 @@ export function normalizeExternalToolSource(raw: { · 228 unmodified lines | |||
| 353 | sourceId: normalizedRaw.id, | 357 | sourceId: normalizedRaw.id, |
| 354 | sourceKey: `source:${normalizedRaw.id}`, | 358 | sourceKey: `source:${normalizedRaw.id}`, |
| 355 | url: config.url, | 359 | url: config.url, |
| 360 | useCredentialedFetch: config.useCredentialedFetch, | ||
| 356 | auth: config.auth, | 361 | auth: config.auth, |
| 357 | transport: config.transport, | 362 | transport: config.transport, |
| 358 | queryParams: config.queryParams, | 363 | queryParams: config.queryParams, |
| @@ -376,6 +381,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines | |||
| 376 | sourceId: normalizedRaw.id, | 381 | sourceId: normalizedRaw.id, |
| 377 | sourceKey: `source:${normalizedRaw.id}`, | 382 | sourceKey: `source:${normalizedRaw.id}`, |
| 378 | endpoint: config.endpoint, | 383 | endpoint: config.endpoint, |
| 384 | useCredentialedFetch: config.useCredentialedFetch, | ||
| 379 | schema: config.schema, | 385 | schema: config.schema, |
| 380 | auth: config.auth, | 386 | auth: config.auth, |
| 381 | defaultQueryApproval: config.defaultQueryApproval, | 387 | defaultQueryApproval: config.defaultQueryApproval, |
| @@ -399,6 +405,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines | |||
| 399 | sourceId: normalizedRaw.id, | 405 | sourceId: normalizedRaw.id, |
| 400 | sourceKey: `source:${normalizedRaw.id}`, | 406 | sourceKey: `source:${normalizedRaw.id}`, |
| 401 | spec: config.spec, | 407 | spec: config.spec, |
| 408 | useCredentialedFetch: config.useCredentialedFetch, | ||
| 402 | collectionUrl: config.collectionUrl, | 409 | collectionUrl: config.collectionUrl, |
| 403 | postmanProxyUrl: config.postmanProxyUrl, | 410 | postmanProxyUrl: config.postmanProxyUrl, |
| 404 | baseUrl: config.baseUrl, | 411 | baseUrl: config.baseUrl, |
| @@ -500,12 +507,91 @@ async function resolveMcpDiscoveryHeaders( · 95 unmodified lines | |||
| 500 | return { headers, warnings: [] }; | 507 | return { headers, warnings: [] }; |
| 501 | } | 508 | } |
| 502 | 509 | ||
| 510 | async function resolveOpenApiFetchHeaders( | ||
| 511 | ctx: ActionCtx, | ||
| 512 | source: OpenApiToolSourceConfig, | ||
| 513 | workspaceId: Id<"workspaces">, | ||
| 514 | accountId?: Id<"accounts">, | ||
| 515 | ): Promise<{ headers: Record<string, string>; warnings: string[] }> { | ||
| 516 | if (!source.useCredentialedFetch) { | ||
| 517 | return { headers: {}, warnings: [] }; | ||
| 518 | } | ||
| 519 | |||
| 520 | const auth = source.auth; | ||
| 521 | if (!auth || auth.type === "none") { | ||
| 522 | return { headers: {}, warnings: [] }; | ||
| 523 | } | ||
| 524 | |||
| 525 | const mode = auth.mode ?? "workspace"; | ||
| 526 | |||
| 527 | if (!source.sourceKey) { | ||
| 528 | return { | ||
| 529 | headers: {}, | ||
| 530 | warnings: [`Source '${source.name}': missing source key for OpenAPI credentialed fetch`], | ||
| 531 | }; | ||
| 532 | } | ||
| 533 | |||
| 534 | const record = await ctx.runQuery(internal.database.resolveCredential, { | ||
| 535 | workspaceId, | ||
| 536 | sourceKey: source.sourceKey, | ||
| 537 | scopeType: mode, | ||
| 538 | accountId, | ||
| 539 | }); | ||
| 540 | |||
| 541 | if (!record) { | ||
| 542 | return { | ||
| 543 | headers: {}, | ||
| 544 | warnings: [`Source '${source.name}': missing ${mode} credential for OpenAPI fetch`], | ||
| 545 | }; | ||
| 546 | } | ||
| 547 | |||
| 548 | const readSecretResult = await resolveCredentialPayloadResult(record, { | ||
| 549 | readVaultObject: async (input) => await readWorkosVaultObjectViaAction(ctx, input), | ||
| 550 | }); | ||
| 551 | if (readSecretResult.isErr()) { | ||
| 552 | return { | ||
| 553 | headers: {}, | ||
| 554 | warnings: [`Source '${source.name}': failed to resolve OpenAPI credential: ${readSecretResult.error.message}`], | ||
| 555 | }; | ||
| 556 | } | ||
| 557 | |||
| 558 | const secret = readSecretResult.value; | ||
| 559 | if (!secret) { | ||
| 560 | return { | ||
| 561 | headers: {}, | ||
| 562 | warnings: [`Source '${source.name}': credential payload unavailable for OpenAPI fetch`], | ||
| 563 | }; | ||
| 564 | } | ||
| 565 | |||
| 566 | const authSpec = toCredentialHeaderSpec(auth); | ||
| 567 | if (!authSpec) { | ||
| 568 | return { headers: {}, warnings: [] }; | ||
| 569 | } | ||
| 570 | |||
| 571 | const headers = buildCredentialAuthHeaders(authSpec, secret); | ||
| 572 | const additionalHeaders = readCredentialAdditionalHeaders(record.additionalHeaders); | ||
| 573 | for (const [key, value] of Object.entries(additionalHeaders)) { | ||
| 574 | headers[key] = value; | ||
| 575 | } | ||
| 576 | |||
| 577 | if (Object.keys(headers).length === 0) { | ||
| 578 | return { | ||
| 579 | headers: {}, | ||
| 580 | warnings: [`Source '${source.name}': credential did not produce OpenAPI auth headers`], | ||
| 581 | }; | ||
| 582 | } | ||
| 583 | |||
| 584 | return { headers, warnings: [] }; | ||
| 585 | } | ||
| 586 | |||
| 503 | async function loadCachedOpenApiSpec( | 587 | async function loadCachedOpenApiSpec( |
| 504 | ctx: ActionCtx, | 588 | ctx: ActionCtx, |
| 505 | specUrl: string, | 589 | specUrl: string, |
| 506 | sourceName: string, | 590 | sourceName: string, |
| 507 | includeDts: boolean, | 591 | includeDts: boolean, |
| 592 | headers?: Record<string, string>, | ||
| 508 | ): Promise<PreparedOpenApiSpec> { | 593 | ): Promise<PreparedOpenApiSpec> { |
| 594 | const hasHeaders = Boolean(headers && Object.keys(headers).length > 0); | ||
| 509 | const getDtsStatus = (prepared: PreparedOpenApiSpec): "ready" | "failed" | "skipped" => { | 595 | const getDtsStatus = (prepared: PreparedOpenApiSpec): "ready" | "failed" | "skipped" => { |
| 510 | if (prepared.dtsStatus) { | 596 | if (prepared.dtsStatus) { |
| 511 | return prepared.dtsStatus; | 597 | return prepared.dtsStatus; |
| @@ -513,44 +599,49 @@ async function loadCachedOpenApiSpec( · 1 unmodified line | |||
| 513 | return prepared.dts ? "ready" : "failed"; | 599 | return prepared.dts ? "ready" : "failed"; |
| 514 | }; | 600 | }; |
| 515 | 601 | ||
| 516 | try { | 602 | if (!hasHeaders) { |
| 517 | const entry = await ctx.runQuery(internal.openApiSpecCache.getEntry, { | 603 | try { |
| 518 | specUrl, | 604 | const entry = await ctx.runQuery(internal.openApiSpecCache.getEntry, { |
| 519 | version: OPENAPI_PREPARED_CACHE_VERSION, | 605 | specUrl, |
| 520 | maxAgeMs: OPENAPI_SPEC_CACHE_TTL_MS, | 606 | version: OPENAPI_PREPARED_CACHE_VERSION, |
| 521 | }); | 607 | maxAgeMs: OPENAPI_SPEC_CACHE_TTL_MS, |
| 608 | }); | ||
| 522 | 609 | ||
| 523 | if (entry) { | 610 | if (entry) { |
| 524 | const blob = await ctx.storage.get(entry.storageId); | 611 | const blob = await ctx.storage.get(entry.storageId); |
| 525 | if (blob) { | 612 | if (blob) { |
| 526 | const json = await blob.text(); | 613 | const json = await blob.text(); |
| 527 | let parsedJson: unknown; | 614 | let parsedJson: unknown; |
| 528 | try { | 615 | try { |
| 529 | parsedJson = JSON.parse(json); | 616 | parsedJson = JSON.parse(json); |
| 530 | } catch { | 617 | } catch { |
| 531 | parsedJson = undefined; | 618 | parsedJson = undefined; |
| 532 | } | 619 | } |
| 533 | 620 | ||
| 534 | if (parsedJson !== undefined) { | 621 | if (parsedJson !== undefined) { |
| 535 | const prepared = toPreparedOpenApiSpec(parsedJson); | 622 | const prepared = toPreparedOpenApiSpec(parsedJson); |
| 536 | if (prepared && (!includeDts || getDtsStatus(prepared) !== "skipped")) { | 623 | if (prepared && (!includeDts || getDtsStatus(prepared) !== "skipped")) { |
| 537 | return prepared; | 624 | return prepared; |
| 625 | } | ||
| 538 | } | 626 | } |
| 539 | } | 627 | } |
| 540 | } | 628 | } |
| 629 | } catch (error) { | ||
| 630 | const message = error instanceof Error ? error.message : String(error); | ||
| 631 | console.warn(`[executor] OpenAPI cache read failed for '${sourceName}': ${message}`); | ||
| 541 | } | 632 | } |
| 542 | } catch (error) { | ||
| 543 | const message = error instanceof Error ? error.message : String(error); | ||
| 544 | console.warn(`[executor] OpenAPI cache read failed for '${sourceName}': ${message}`); | ||
| 545 | } | 633 | } |
| 546 | 634 | ||
| 547 | const externalPrepared = await loadPreparedOpenApiSpecFromExternalGenerate( | 635 | const externalPrepared = await loadPreparedOpenApiSpecFromExternalGenerate( |
| 548 | specUrl, | 636 | specUrl, |
| 549 | sourceName, | 637 | sourceName, |
| 550 | includeDts, | 638 | includeDts, |
| 639 | headers, | ||
| 551 | ); | 640 | ); |
| 552 | if (externalPrepared) { | 641 | if (externalPrepared) { |
| 553 | await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared); | 642 | if (!hasHeaders) { |
| 643 | await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared); | ||
| 644 | } | ||
| 554 | return externalPrepared; | 645 | return externalPrepared; |
| 555 | } | 646 | } |
| 556 | 647 | ||
| @@ -615,14 +706,16 @@ async function loadCachedOpenApiSpec( · 58 unmodified lines | |||
| 615 | throw new Error(`Prepared OpenAPI payload for '${sourceName}' was invalid`); | 706 | throw new Error(`Prepared OpenAPI payload for '${sourceName}' was invalid`); |
| 616 | } | 707 | } |
| 617 | 708 | ||
| 618 | await cachePreparedOpenApiSpec( | 709 | if (!hasHeaders) { |
| 619 | ctx, | 710 | await cachePreparedOpenApiSpec( |
| 620 | specUrl, | 711 | ctx, |
| 621 | sourceName, | 712 | specUrl, |
| 622 | prepared, | 713 | sourceName, |
| 623 | preparedStorageId, | 714 | prepared, |
| 624 | preparedSizeBytes, | 715 | preparedStorageId, |
| 625 | ); | 716 | preparedSizeBytes, |
| 717 | ); | ||
| 718 | } | ||
| 626 | 719 | ||
| 627 | return prepared; | 720 | return prepared; |
| 628 | } | 721 | } |
| @@ -636,7 +729,15 @@ export async function loadSourceArtifact( · 7 unmodified lines | |||
| 636 | 729 | ||
| 637 | if (source.type === "openapi" && typeof source.spec === "string") { | 730 | if (source.type === "openapi" && typeof source.spec === "string") { |
| 638 | try { | 731 | try { |
| 639 | const sharedArtifactCacheEligible = canUseSharedOpenApiArtifactCache(source); | 732 | const resolvedOpenApiHeaders = await resolveOpenApiFetchHeaders( |
| 733 | ctx, | ||
| 734 | source, | ||
| 735 | options.workspaceId, | ||
| 736 | options.accountId, | ||
| 737 | ); | ||
| 738 | |||
| 739 | const sharedArtifactCacheEligible = canUseSharedOpenApiArtifactCache(source) | ||
| 740 | && Object.keys(resolvedOpenApiHeaders.headers).length === 0; | ||
| 640 | const sharedArtifactCacheKey = sharedArtifactCacheEligible | 741 | const sharedArtifactCacheKey = sharedArtifactCacheEligible |
| 641 | ? sharedOpenApiArtifactCacheKey(source) | 742 | ? sharedOpenApiArtifactCacheKey(source) |
| 642 | : undefined; | 743 | : undefined; |
| @@ -652,16 +753,25 @@ export async function loadSourceArtifact( · 9 unmodified lines | |||
| 652 | } | 753 | } |
| 653 | } | 754 | } |
| 654 | 755 | ||
| 655 | const prepared = await loadCachedOpenApiSpec(ctx, source.spec, source.name, includeDts); | 756 | const prepared = await loadCachedOpenApiSpec( |
| 757 | ctx, | ||
| 758 | source.spec, | ||
| 759 | source.name, | ||
| 760 | includeDts, | ||
| 761 | resolvedOpenApiHeaders.headers, | ||
| 762 | ); | ||
| 656 | const artifact = compileOpenApiArtifactFromPrepared(source, prepared); | 763 | const artifact = compileOpenApiArtifactFromPrepared(source, prepared); |
| 657 | 764 | ||
| 658 | if (sharedArtifactCacheEligible && sharedArtifactCacheKey) { | 765 | if (sharedArtifactCacheEligible && sharedArtifactCacheKey) { |
| 659 | await putSharedOpenApiArtifactCache(ctx, sharedArtifactCacheKey, artifact); | 766 | await putSharedOpenApiArtifactCache(ctx, sharedArtifactCacheKey, artifact); |
| 660 | } | 767 | } |
| 661 | 768 | ||
| 662 | const warnings = (prepared.warnings ?? []).map( | 769 | const warnings = [ |
| 770 | ...resolvedOpenApiHeaders.warnings, | ||
| 771 | ...(prepared.warnings ?? []).map( | ||
| 663 | (warning) => `Source '${source.name}': ${warning}`, | 772 | (warning) => `Source '${source.name}': ${warning}`, |
| 664 | ); | 773 | ), |
| 774 | ]; | ||
| 665 | return { | 775 | return { |
| 666 | artifact, | 776 | artifact, |
| 667 | warnings, | 777 | warnings, |