Unified · Split
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /// <reference types="next" /> |
| 2 | 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 | 5 | // NOTE: This file should not be edited |
| 6 | 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. |
| @@ -1,9 +1,7 @@ | ||
| 1 | 1 | import { parse as parseYaml } from "yaml"; |
| 2 | 2 | import { z } from "zod"; |
| 3 | −import { unstable_cache } from "next/cache"; | |
| 4 | 3 | import { prepareOpenApiSpec } from "@executor/core/openapi-prepare"; |
| 5 | 4 | |
| 6 | −const GENERATED_SPEC_REVALIDATE_SECONDS = 60 * 60; | |
| 7 | 5 | const HEADER_FETCH_TIMEOUT_MS = 12_000; |
| 8 | 6 | |
| 9 | 7 | const querySchema = z.object({ |
| @@ -26,17 +24,6 @@ const blockedForwardedHeaderNames = new Set([ · 16 unmodified lines | ||
| 26 | 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 | 27 | function jsonResponse( |
| 41 | 28 | payload: unknown, |
| 42 | 29 | status: number, |
| @@ -172,7 +159,10 @@ export async function GET(request: Request): Promise<Response> { · 129 unmodified lines | ||
| 172 | 159 | const forwardedHeaders = sanitizeForwardHeaders(parseHeadersQueryValue(parsed.data.headers)); |
| 173 | 160 | |
| 174 | 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 | 166 | : await prepareWithForwardedHeaders( |
| 177 | 167 | parsed.data.specUrl, |
| 178 | 168 | sourceName, |
| @@ -144,6 +144,7 @@ export function AddSourceDialog({ · 143 unmodified lines | ||
| 144 | 144 | endpoint: form.endpoint.trim(), |
| 145 | 145 | baseUrl: form.baseUrl, |
| 146 | 146 | auth: form.buildAuthConfig(), |
| 147 | + useCredentialedFetch: form.useCredentialedFetch, | |
| 147 | 148 | mcpTransport: form.mcpTransport, |
| 148 | 149 | accountId: context.accountId, |
| 149 | 150 | }); |
| @@ -227,6 +228,7 @@ export function AddSourceDialog({ · 77 unmodified lines | ||
| 227 | 228 | authType: form.authType, |
| 228 | 229 | authScope: form.authScope, |
| 229 | 230 | apiKeyHeader: form.apiKeyHeader, |
| 231 | + useCredentialedFetch: form.useCredentialedFetch, | |
| 230 | 232 | existingScopedCredential: form.existingScopedCredential, |
| 231 | 233 | buildAuthConfig: form.buildAuthConfig, |
| 232 | 234 | hasCredentialInput: form.hasCredentialInput, |
| @@ -35,6 +35,7 @@ type SaveFormSnapshot = { · 34 unmodified lines | ||
| 35 | 35 | authType: Exclude<SourceAuthType, "mixed">; |
| 36 | 36 | authScope: CredentialScope; |
| 37 | 37 | apiKeyHeader: string; |
| 38 | + useCredentialedFetch: boolean; | |
| 38 | 39 | existingScopedCredential: CredentialRecord | null; |
| 39 | 40 | buildAuthConfig: () => Record<string, unknown> | undefined; |
| 40 | 41 | hasCredentialInput: () => boolean; |
| @@ -120,6 +121,7 @@ export async function saveSourceWithCredentials({ · 79 unmodified lines | ||
| 120 | 121 | endpoint: form.endpoint.trim(), |
| 121 | 122 | baseUrl: form.baseUrl, |
| 122 | 123 | auth: authConfig, |
| 124 | + useCredentialedFetch: form.useCredentialedFetch, | |
| 123 | 125 | mcpTransport: form.mcpTransport, |
| 124 | 126 | accountId: context.accountId, |
| 125 | 127 | }); |
| @@ -172,19 +172,17 @@ export function SourceAuthPanel({ · 171 unmodified lines | ||
| 172 | 172 | </p> |
| 173 | 173 | ) : null} |
| 174 | 174 | |
| 175 | − {sourceType === "openapi" ? ( | |
| 176 | − <label className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2"> | |
| 177 | − <input | |
| 178 | − type="checkbox" | |
| 179 | − checked={useCredentialedFetch} | |
| 180 | − onChange={(event) => onUseCredentialedFetchChange(event.target.checked)} | |
| 181 | − className="h-3.5 w-3.5" | |
| 182 | − /> | |
| 183 | − <span className="text-[11px] text-muted-foreground"> | |
| 184 | − Use credentials while detecting/fetching spec (off by default for shared cache speed) | |
| 185 | − </span> | |
| 186 | − </label> | |
| 187 | − ) : null} | |
| 175 | + <label className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2"> | |
| 176 | + <input | |
| 177 | + type="checkbox" | |
| 178 | + checked={useCredentialedFetch} | |
| 179 | + onChange={(event) => onUseCredentialedFetchChange(event.target.checked)} | |
| 180 | + className="h-3.5 w-3.5" | |
| 181 | + /> | |
| 182 | + <span className="text-[11px] text-muted-foreground"> | |
| 183 | + Use credentials while fetching source metadata | |
| 184 | + </span> | |
| 185 | + </label> | |
| 188 | 186 | |
| 189 | 187 | {sourceType === "mcp" && mcpOAuthStatus === "checking" ? ( |
| 190 | 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 | 42 | endpoint, |
| 43 | 43 | baseUrl, |
| 44 | 44 | auth, |
| 45 | + useCredentialedFetch, | |
| 45 | 46 | mcpTransport, |
| 46 | 47 | accountId, |
| 47 | 48 | }: { |
| @@ -49,6 +50,7 @@ export function createCustomSourceConfig({ · 1 unmodified line | ||
| 49 | 50 | endpoint: string; |
| 50 | 51 | baseUrl: string; |
| 51 | 52 | auth?: Record<string, unknown>; |
| 53 | + useCredentialedFetch: boolean; | |
| 52 | 54 | mcpTransport: "auto" | "streamable-http" | "sse"; |
| 53 | 55 | accountId?: string; |
| 54 | 56 | }): Record<string, unknown> { |
| @@ -57,6 +59,7 @@ export function createCustomSourceConfig({ · 2 unmodified lines | ||
| 57 | 59 | if (type === "mcp") { |
| 58 | 60 | return { |
| 59 | 61 | url: normalizedEndpoint, |
| 62 | + useCredentialedFetch, | |
| 60 | 63 | ...(auth ? { auth } : {}), |
| 61 | 64 | ...(mcpTransport !== "auto" ? { transport: mcpTransport } : {}), |
| 62 | 65 | ...(accountId |
| @@ -68,6 +71,7 @@ export function createCustomSourceConfig({ · 5 unmodified lines | ||
| 68 | 71 | if (type === "graphql") { |
| 69 | 72 | return { |
| 70 | 73 | endpoint: normalizedEndpoint, |
| 74 | + useCredentialedFetch, | |
| 71 | 75 | ...(auth ? { auth } : {}), |
| 72 | 76 | }; |
| 73 | 77 | } |
| @@ -75,6 +79,7 @@ export function createCustomSourceConfig({ · 1 unmodified line | ||
| 75 | 79 | return { |
| 76 | 80 | spec: normalizedEndpoint, |
| 77 | 81 | specUrl: normalizedEndpoint, |
| 82 | + useCredentialedFetch, | |
| 78 | 83 | ...(baseUrl ? { baseUrl } : {}), |
| 79 | 84 | ...(auth ? { auth } : {}), |
| 80 | 85 | }; |
| @@ -178,7 +178,7 @@ export function sourceToFormValues(source: ToolSourceRecord): SourceFormValues { · 177 unmodified lines | ||
| 178 | 178 | apiKeyValue: "", |
| 179 | 179 | basicUsername: "", |
| 180 | 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 | 182 | endpoint: form.endpoint.trim(), |
| 183 | 183 | baseUrl: form.baseUrl, |
| 184 | 184 | auth: form.buildAuthConfig(), |
| 185 | + useCredentialedFetch: form.useCredentialedFetch, | |
| 185 | 186 | mcpTransport: form.mcpTransport, |
| 186 | 187 | accountId: context.accountId, |
| 187 | 188 | }); |
| @@ -264,6 +265,7 @@ export function SourceFormPanel({ · 76 unmodified lines | ||
| 264 | 265 | authType: form.authType, |
| 265 | 266 | authScope: form.authScope, |
| 266 | 267 | apiKeyHeader: form.apiKeyHeader, |
| 268 | + useCredentialedFetch: form.useCredentialedFetch, | |
| 267 | 269 | existingScopedCredential: form.existingScopedCredential, |
| 268 | 270 | buildAuthConfig: form.buildAuthConfig, |
| 269 | 271 | hasCredentialInput: form.hasCredentialInput, |
| @@ -6,6 +6,7 @@ export interface McpToolSourceConfig { · 5 unmodified lines | ||
| 6 | 6 | sourceId?: string; |
| 7 | 7 | sourceKey?: string; |
| 8 | 8 | url: string; |
| 9 | + useCredentialedFetch?: boolean; | |
| 9 | 10 | auth?: OpenApiAuth; |
| 10 | 11 | discoveryHeaders?: Record<string, string>; |
| 11 | 12 | transport?: "sse" | "streamable-http"; |
| @@ -26,6 +27,7 @@ export interface OpenApiToolSourceConfig { · 14 unmodified lines | ||
| 26 | 27 | sourceId?: string; |
| 27 | 28 | sourceKey?: string; |
| 28 | 29 | spec: string | Record<string, unknown>; |
| 30 | + useCredentialedFetch?: boolean; | |
| 29 | 31 | collectionUrl?: string; |
| 30 | 32 | postmanProxyUrl?: string; |
| 31 | 33 | baseUrl?: string; |
| @@ -41,6 +43,7 @@ export interface GraphqlToolSourceConfig { · 9 unmodified lines | ||
| 41 | 43 | sourceId?: string; |
| 42 | 44 | sourceKey?: string; |
| 43 | 45 | endpoint: string; |
| 46 | + useCredentialedFetch?: boolean; | |
| 44 | 47 | schema?: Record<string, unknown>; |
| 45 | 48 | auth?: OpenApiAuth; |
| 46 | 49 | defaultQueryApproval?: ToolApprovalMode; |
| @@ -392,6 +392,41 @@ export const resolveCredential = internalQuery({ · 391 unmodified lines | ||
| 392 | 392 | } |
| 393 | 393 | |
| 394 | 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 | 431 | const tryWorkspace = async () => { |
| 397 | 432 | return await ctx.db |
| @@ -13,17 +13,17 @@ export const TOOL_SOURCE_CONFIG_VERSION = 1; · 12 unmodified lines | ||
| 13 | 13 | |
| 14 | 14 | export type NormalizedMcpToolSourceConfig = Pick< |
| 15 | 15 | McpToolSourceConfig, |
| 16 | − "url" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides" | |
| 16 | + "url" | "useCredentialedFetch" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides" | |
| 17 | 17 | >; |
| 18 | 18 | |
| 19 | 19 | export type NormalizedGraphqlToolSourceConfig = Pick< |
| 20 | 20 | GraphqlToolSourceConfig, |
| 21 | − "endpoint" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides" | |
| 21 | + "endpoint" | "useCredentialedFetch" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides" | |
| 22 | 22 | >; |
| 23 | 23 | |
| 24 | 24 | export type NormalizedOpenApiToolSourceConfig = Pick< |
| 25 | 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 | 29 | export type NormalizedToolSourceConfig = |
| @@ -315,6 +315,7 @@ export function normalizeToolSourceConfig( · 285 unmodified lines | ||
| 315 | 315 | |
| 316 | 316 | return Result.ok({ |
| 317 | 317 | url: urlResult.value, |
| 318 | + useCredentialedFetch: config.useCredentialedFetch === true, | |
| 318 | 319 | transport, |
| 319 | 320 | auth: authResult.value, |
| 320 | 321 | queryParams: queryParamsResult.value, |
| @@ -361,6 +362,7 @@ export function normalizeToolSourceConfig( · 40 unmodified lines | ||
| 361 | 362 | |
| 362 | 363 | return Result.ok({ |
| 363 | 364 | endpoint: endpointResult.value, |
| 365 | + useCredentialedFetch: config.useCredentialedFetch === true, | |
| 364 | 366 | schema: Object.keys(schema).length > 0 ? schema : undefined, |
| 365 | 367 | auth: authResult.value, |
| 366 | 368 | defaultQueryApproval: defaultQueryApprovalResult.value, |
| @@ -402,6 +404,7 @@ export function normalizeToolSourceConfig( · 35 unmodified lines | ||
| 402 | 404 | |
| 403 | 405 | return Result.ok({ |
| 404 | 406 | spec: specResult.value, |
| 407 | + useCredentialedFetch: config.useCredentialedFetch === true, | |
| 405 | 408 | collectionUrl: optionalTrimmedString(config.collectionUrl), |
| 406 | 409 | postmanProxyUrl: optionalTrimmedString(config.postmanProxyUrl), |
| 407 | 410 | baseUrl: optionalTrimmedString(config.baseUrl), |
| @@ -105,6 +105,7 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 104 unmodified lines | ||
| 105 | 105 | specUrl: string, |
| 106 | 106 | sourceName: string, |
| 107 | 107 | includeDts: boolean, |
| 108 | + headers?: Record<string, string>, | |
| 108 | 109 | ): Promise<PreparedOpenApiSpec | null> { |
| 109 | 110 | const endpoint = resolveExternalGenerateEndpoint(); |
| 110 | 111 | if (!endpoint) { |
| @@ -119,6 +120,9 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 8 unmodified lines | ||
| 119 | 120 | url.searchParams.set("specUrl", specUrl); |
| 120 | 121 | url.searchParams.set("sourceName", sourceName); |
| 121 | 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 | 127 | const response = await fetch(url.toString(), { |
| 124 | 128 | method: "GET", |
| @@ -353,6 +357,7 @@ export function normalizeExternalToolSource(raw: { · 228 unmodified lines | ||
| 353 | 357 | sourceId: normalizedRaw.id, |
| 354 | 358 | sourceKey: `source:${normalizedRaw.id}`, |
| 355 | 359 | url: config.url, |
| 360 | + useCredentialedFetch: config.useCredentialedFetch, | |
| 356 | 361 | auth: config.auth, |
| 357 | 362 | transport: config.transport, |
| 358 | 363 | queryParams: config.queryParams, |
| @@ -376,6 +381,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines | ||
| 376 | 381 | sourceId: normalizedRaw.id, |
| 377 | 382 | sourceKey: `source:${normalizedRaw.id}`, |
| 378 | 383 | endpoint: config.endpoint, |
| 384 | + useCredentialedFetch: config.useCredentialedFetch, | |
| 379 | 385 | schema: config.schema, |
| 380 | 386 | auth: config.auth, |
| 381 | 387 | defaultQueryApproval: config.defaultQueryApproval, |
| @@ -399,6 +405,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines | ||
| 399 | 405 | sourceId: normalizedRaw.id, |
| 400 | 406 | sourceKey: `source:${normalizedRaw.id}`, |
| 401 | 407 | spec: config.spec, |
| 408 | + useCredentialedFetch: config.useCredentialedFetch, | |
| 402 | 409 | collectionUrl: config.collectionUrl, |
| 403 | 410 | postmanProxyUrl: config.postmanProxyUrl, |
| 404 | 411 | baseUrl: config.baseUrl, |
| @@ -500,12 +507,91 @@ async function resolveMcpDiscoveryHeaders( · 95 unmodified lines | ||
| 500 | 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 | 587 | async function loadCachedOpenApiSpec( |
| 504 | 588 | ctx: ActionCtx, |
| 505 | 589 | specUrl: string, |
| 506 | 590 | sourceName: string, |
| 507 | 591 | includeDts: boolean, |
| 592 | + headers?: Record<string, string>, | |
| 508 | 593 | ): Promise<PreparedOpenApiSpec> { |
| 594 | + const hasHeaders = Boolean(headers && Object.keys(headers).length > 0); | |
| 509 | 595 | const getDtsStatus = (prepared: PreparedOpenApiSpec): "ready" | "failed" | "skipped" => { |
| 510 | 596 | if (prepared.dtsStatus) { |
| 511 | 597 | return prepared.dtsStatus; |
| @@ -513,44 +599,49 @@ async function loadCachedOpenApiSpec( · 1 unmodified line | ||
| 513 | 599 | return prepared.dts ? "ready" : "failed"; |
| 514 | 600 | }; |
| 515 | 601 | |
| 516 | − try { | |
| 517 | − const entry = await ctx.runQuery(internal.openApiSpecCache.getEntry, { | |
| 518 | − specUrl, | |
| 519 | − version: OPENAPI_PREPARED_CACHE_VERSION, | |
| 520 | − maxAgeMs: OPENAPI_SPEC_CACHE_TTL_MS, | |
| 521 | − }); | |
| 602 | + if (!hasHeaders) { | |
| 603 | + try { | |
| 604 | + const entry = await ctx.runQuery(internal.openApiSpecCache.getEntry, { | |
| 605 | + specUrl, | |
| 606 | + version: OPENAPI_PREPARED_CACHE_VERSION, | |
| 607 | + maxAgeMs: OPENAPI_SPEC_CACHE_TTL_MS, | |
| 608 | + }); | |
| 522 | 609 | |
| 523 | − if (entry) { | |
| 524 | − const blob = await ctx.storage.get(entry.storageId); | |
| 525 | − if (blob) { | |
| 526 | − const json = await blob.text(); | |
| 527 | − let parsedJson: unknown; | |
| 528 | − try { | |
| 529 | − parsedJson = JSON.parse(json); | |
| 530 | − } catch { | |
| 531 | − parsedJson = undefined; | |
| 532 | − } | |
| 610 | + if (entry) { | |
| 611 | + const blob = await ctx.storage.get(entry.storageId); | |
| 612 | + if (blob) { | |
| 613 | + const json = await blob.text(); | |
| 614 | + let parsedJson: unknown; | |
| 615 | + try { | |
| 616 | + parsedJson = JSON.parse(json); | |
| 617 | + } catch { | |
| 618 | + parsedJson = undefined; | |
| 619 | + } | |
| 533 | 620 | |
| 534 | − if (parsedJson !== undefined) { | |
| 535 | − const prepared = toPreparedOpenApiSpec(parsedJson); | |
| 536 | − if (prepared && (!includeDts || getDtsStatus(prepared) !== "skipped")) { | |
| 537 | − return prepared; | |
| 621 | + if (parsedJson !== undefined) { | |
| 622 | + const prepared = toPreparedOpenApiSpec(parsedJson); | |
| 623 | + if (prepared && (!includeDts || getDtsStatus(prepared) !== "skipped")) { | |
| 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 | 635 | const externalPrepared = await loadPreparedOpenApiSpecFromExternalGenerate( |
| 548 | 636 | specUrl, |
| 549 | 637 | sourceName, |
| 550 | 638 | includeDts, |
| 639 | + headers, | |
| 551 | 640 | ); |
| 552 | 641 | if (externalPrepared) { |
| 553 | − await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared); | |
| 642 | + if (!hasHeaders) { | |
| 643 | + await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared); | |
| 644 | + } | |
| 554 | 645 | return externalPrepared; |
| 555 | 646 | } |
| 556 | 647 | |
| @@ -615,14 +706,16 @@ async function loadCachedOpenApiSpec( · 58 unmodified lines | ||
| 615 | 706 | throw new Error(`Prepared OpenAPI payload for '${sourceName}' was invalid`); |
| 616 | 707 | } |
| 617 | 708 | |
| 618 | − await cachePreparedOpenApiSpec( | |
| 619 | − ctx, | |
| 620 | − specUrl, | |
| 621 | − sourceName, | |
| 622 | − prepared, | |
| 623 | − preparedStorageId, | |
| 624 | − preparedSizeBytes, | |
| 625 | − ); | |
| 709 | + if (!hasHeaders) { | |
| 710 | + await cachePreparedOpenApiSpec( | |
| 711 | + ctx, | |
| 712 | + specUrl, | |
| 713 | + sourceName, | |
| 714 | + prepared, | |
| 715 | + preparedStorageId, | |
| 716 | + preparedSizeBytes, | |
| 717 | + ); | |
| 718 | + } | |
| 626 | 719 | |
| 627 | 720 | return prepared; |
| 628 | 721 | } |
| @@ -636,7 +729,15 @@ export async function loadSourceArtifact( · 7 unmodified lines | ||
| 636 | 729 | |
| 637 | 730 | if (source.type === "openapi" && typeof source.spec === "string") { |
| 638 | 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 | 741 | const sharedArtifactCacheKey = sharedArtifactCacheEligible |
| 641 | 742 | ? sharedOpenApiArtifactCacheKey(source) |
| 642 | 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 | 763 | const artifact = compileOpenApiArtifactFromPrepared(source, prepared); |
| 657 | 764 | |
| 658 | 765 | if (sharedArtifactCacheEligible && sharedArtifactCacheKey) { |
| 659 | 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 | 772 | (warning) => `Source '${source.name}': ${warning}`, |
| 664 | − ); | |
| 773 | + ), | |
| 774 | + ]; | |
| 665 | 775 | return { |
| 666 | 776 | artifact, |
| 667 | 777 | warnings, |