History

fix

Rhys Sullivan committed 8 months ago

Commit dfd0a1f1 parent 25cd57a12 files · +218 −68Browse files

Unified · Split

12 files
  1. executor/apps/web/next-env.d.ts +1 −1
  2. executor/apps/web/src/app/api/generate/route.ts +4 −14
  3. executor/apps/web/src/components/tools/add/source-dialog.tsx +2 −0
  4. executor/apps/web/src/components/tools/add/source-submit.ts +2 −0
  5. executor/apps/web/src/components/tools/add/source/auth-panel.tsx +11 −13
  6. executor/apps/web/src/components/tools/add/source/dialog-helpers.ts +5 −0
  7. executor/apps/web/src/components/tools/add/source/form-utils.ts +1 −1
  8. executor/apps/web/src/components/tools/explorer/source-form-panel.tsx +2 −0
  9. executor/packages/core/src/tool/source-types.ts +3 −0
  10. executor/packages/database/convex/database/credentials.ts +35 −0
  11. executor/packages/database/src/database/tool_source_config.ts +6 −3
  12. executor/packages/database/src/runtime/tool_source_loading.ts +146 −36
executor/apps/web/next-env.d.ts+1−1
@@ -1,6 +1,6 @@
11 /// <reference types="next" />
22 /// <reference types="next/image-types/global" />
3−import "./.next/types/routes.d.ts";
3+import "./.next/dev/types/routes.d.ts";
44
55 // NOTE: This file should not be edited
66 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
executor/apps/web/src/app/api/generate/route.ts+4−14
@@ -1,9 +1,7 @@
11 import { parse as parseYaml } from "yaml";
22 import { z } from "zod";
3−import { unstable_cache } from "next/cache";
43 import { prepareOpenApiSpec } from "@executor/core/openapi-prepare";
54
6−const GENERATED_SPEC_REVALIDATE_SECONDS = 60 * 60;
75 const HEADER_FETCH_TIMEOUT_MS = 12_000;
86
97 const querySchema = z.object({
@@ -26,17 +24,6 @@ const blockedForwardedHeaderNames = new Set([ · 16 unmodified lines
2624 "referer",
2725 ]);
2826
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−
4027 function jsonResponse(
4128 payload: unknown,
4229 status: number,
@@ -172,7 +159,10 @@ export async function GET(request: Request): Promise<Response> { · 129 unmodified lines
172159 const forwardedHeaders = sanitizeForwardHeaders(parseHeadersQueryValue(parsed.data.headers));
173160
174161 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+ })
176166 : await prepareWithForwardedHeaders(
177167 parsed.data.specUrl,
178168 sourceName,
executor/apps/web/src/components/tools/add/source-dialog.tsx+2−0
@@ -144,6 +144,7 @@ export function AddSourceDialog({ · 143 unmodified lines
144144 endpoint: form.endpoint.trim(),
145145 baseUrl: form.baseUrl,
146146 auth: form.buildAuthConfig(),
147+ useCredentialedFetch: form.useCredentialedFetch,
147148 mcpTransport: form.mcpTransport,
148149 accountId: context.accountId,
149150 });
@@ -227,6 +228,7 @@ export function AddSourceDialog({ · 77 unmodified lines
227228 authType: form.authType,
228229 authScope: form.authScope,
229230 apiKeyHeader: form.apiKeyHeader,
231+ useCredentialedFetch: form.useCredentialedFetch,
230232 existingScopedCredential: form.existingScopedCredential,
231233 buildAuthConfig: form.buildAuthConfig,
232234 hasCredentialInput: form.hasCredentialInput,
executor/apps/web/src/components/tools/add/source-submit.ts+2−0
@@ -35,6 +35,7 @@ type SaveFormSnapshot = { · 34 unmodified lines
3535 authType: Exclude<SourceAuthType, "mixed">;
3636 authScope: CredentialScope;
3737 apiKeyHeader: string;
38+ useCredentialedFetch: boolean;
3839 existingScopedCredential: CredentialRecord | null;
3940 buildAuthConfig: () => Record<string, unknown> | undefined;
4041 hasCredentialInput: () => boolean;
@@ -120,6 +121,7 @@ export async function saveSourceWithCredentials({ · 79 unmodified lines
120121 endpoint: form.endpoint.trim(),
121122 baseUrl: form.baseUrl,
122123 auth: authConfig,
124+ useCredentialedFetch: form.useCredentialedFetch,
123125 mcpTransport: form.mcpTransport,
124126 accountId: context.accountId,
125127 });
executor/apps/web/src/components/tools/add/source/auth-panel.tsx+11−13
@@ -172,19 +172,17 @@ export function SourceAuthPanel({ · 171 unmodified lines
172172 </p>
173173 ) : null}
174174
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>
188186
189187 {sourceType === "mcp" && mcpOAuthStatus === "checking" ? (
190188 <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
executor/apps/web/src/components/tools/add/source/dialog-helpers.ts+5−0
@@ -42,6 +42,7 @@ export function createCustomSourceConfig({ · 41 unmodified lines
4242 endpoint,
4343 baseUrl,
4444 auth,
45+ useCredentialedFetch,
4546 mcpTransport,
4647 accountId,
4748 }: {
@@ -49,6 +50,7 @@ export function createCustomSourceConfig({ · 1 unmodified line
4950 endpoint: string;
5051 baseUrl: string;
5152 auth?: Record<string, unknown>;
53+ useCredentialedFetch: boolean;
5254 mcpTransport: "auto" | "streamable-http" | "sse";
5355 accountId?: string;
5456 }): Record<string, unknown> {
@@ -57,6 +59,7 @@ export function createCustomSourceConfig({ · 2 unmodified lines
5759 if (type === "mcp") {
5860 return {
5961 url: normalizedEndpoint,
62+ useCredentialedFetch,
6063 ...(auth ? { auth } : {}),
6164 ...(mcpTransport !== "auto" ? { transport: mcpTransport } : {}),
6265 ...(accountId
@@ -68,6 +71,7 @@ export function createCustomSourceConfig({ · 5 unmodified lines
6871 if (type === "graphql") {
6972 return {
7073 endpoint: normalizedEndpoint,
74+ useCredentialedFetch,
7175 ...(auth ? { auth } : {}),
7276 };
7377 }
@@ -75,6 +79,7 @@ export function createCustomSourceConfig({ · 1 unmodified line
7579 return {
7680 spec: normalizedEndpoint,
7781 specUrl: normalizedEndpoint,
82+ useCredentialedFetch,
7883 ...(baseUrl ? { baseUrl } : {}),
7984 ...(auth ? { auth } : {}),
8085 };
executor/apps/web/src/components/tools/add/source/form-utils.ts+1−1
@@ -178,7 +178,7 @@ export function sourceToFormValues(source: ToolSourceRecord): SourceFormValues { · 177 unmodified lines
178178 apiKeyValue: "",
179179 basicUsername: "",
180180 basicPassword: "",
181− useCredentialedFetch: false,
181+ useCredentialedFetch: source.config.useCredentialedFetch === true,
182182 };
183183 }
184184
executor/apps/web/src/components/tools/explorer/source-form-panel.tsx+2−0
@@ -182,6 +182,7 @@ export function SourceFormPanel({ · 181 unmodified lines
182182 endpoint: form.endpoint.trim(),
183183 baseUrl: form.baseUrl,
184184 auth: form.buildAuthConfig(),
185+ useCredentialedFetch: form.useCredentialedFetch,
185186 mcpTransport: form.mcpTransport,
186187 accountId: context.accountId,
187188 });
@@ -264,6 +265,7 @@ export function SourceFormPanel({ · 76 unmodified lines
264265 authType: form.authType,
265266 authScope: form.authScope,
266267 apiKeyHeader: form.apiKeyHeader,
268+ useCredentialedFetch: form.useCredentialedFetch,
267269 existingScopedCredential: form.existingScopedCredential,
268270 buildAuthConfig: form.buildAuthConfig,
269271 hasCredentialInput: form.hasCredentialInput,
executor/packages/core/src/tool/source-types.ts+3−0
@@ -6,6 +6,7 @@ export interface McpToolSourceConfig { · 5 unmodified lines
66 sourceId?: string;
77 sourceKey?: string;
88 url: string;
9+ useCredentialedFetch?: boolean;
910 auth?: OpenApiAuth;
1011 discoveryHeaders?: Record<string, string>;
1112 transport?: "sse" | "streamable-http";
@@ -26,6 +27,7 @@ export interface OpenApiToolSourceConfig { · 14 unmodified lines
2627 sourceId?: string;
2728 sourceKey?: string;
2829 spec: string | Record<string, unknown>;
30+ useCredentialedFetch?: boolean;
2931 collectionUrl?: string;
3032 postmanProxyUrl?: string;
3133 baseUrl?: string;
@@ -41,6 +43,7 @@ export interface GraphqlToolSourceConfig { · 9 unmodified lines
4143 sourceId?: string;
4244 sourceKey?: string;
4345 endpoint: string;
46+ useCredentialedFetch?: boolean;
4447 schema?: Record<string, unknown>;
4548 auth?: OpenApiAuth;
4649 defaultQueryApproval?: ToolApprovalMode;
executor/packages/database/convex/database/credentials.ts+35−0
@@ -392,6 +392,41 @@ export const resolveCredential = internalQuery({ · 391 unmodified lines
392392 }
393393
394394 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+ }
395430
396431 const tryWorkspace = async () => {
397432 return await ctx.db
executor/packages/database/src/database/tool_source_config.ts+6−3
@@ -13,17 +13,17 @@ export const TOOL_SOURCE_CONFIG_VERSION = 1; · 12 unmodified lines
1313
1414 export type NormalizedMcpToolSourceConfig = Pick<
1515 McpToolSourceConfig,
16− "url" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides"
16+ "url" | "useCredentialedFetch" | "auth" | "discoveryHeaders" | "transport" | "queryParams" | "defaultApproval" | "overrides"
1717 >;
1818
1919 export type NormalizedGraphqlToolSourceConfig = Pick<
2020 GraphqlToolSourceConfig,
21− "endpoint" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides"
21+ "endpoint" | "useCredentialedFetch" | "schema" | "auth" | "defaultQueryApproval" | "defaultMutationApproval" | "overrides"
2222 >;
2323
2424 export type NormalizedOpenApiToolSourceConfig = Pick<
2525 OpenApiToolSourceConfig,
26− "spec" | "collectionUrl" | "postmanProxyUrl" | "baseUrl" | "auth" | "defaultReadApproval" | "defaultWriteApproval" | "overrides"
26+ "spec" | "useCredentialedFetch" | "collectionUrl" | "postmanProxyUrl" | "baseUrl" | "auth" | "defaultReadApproval" | "defaultWriteApproval" | "overrides"
2727 >;
2828
2929 export type NormalizedToolSourceConfig =
@@ -315,6 +315,7 @@ export function normalizeToolSourceConfig( · 285 unmodified lines
315315
316316 return Result.ok({
317317 url: urlResult.value,
318+ useCredentialedFetch: config.useCredentialedFetch === true,
318319 transport,
319320 auth: authResult.value,
320321 queryParams: queryParamsResult.value,
@@ -361,6 +362,7 @@ export function normalizeToolSourceConfig( · 40 unmodified lines
361362
362363 return Result.ok({
363364 endpoint: endpointResult.value,
365+ useCredentialedFetch: config.useCredentialedFetch === true,
364366 schema: Object.keys(schema).length > 0 ? schema : undefined,
365367 auth: authResult.value,
366368 defaultQueryApproval: defaultQueryApprovalResult.value,
@@ -402,6 +404,7 @@ export function normalizeToolSourceConfig( · 35 unmodified lines
402404
403405 return Result.ok({
404406 spec: specResult.value,
407+ useCredentialedFetch: config.useCredentialedFetch === true,
405408 collectionUrl: optionalTrimmedString(config.collectionUrl),
406409 postmanProxyUrl: optionalTrimmedString(config.postmanProxyUrl),
407410 baseUrl: optionalTrimmedString(config.baseUrl),
executor/packages/database/src/runtime/tool_source_loading.ts+146−36
@@ -105,6 +105,7 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 104 unmodified lines
105105 specUrl: string,
106106 sourceName: string,
107107 includeDts: boolean,
108+ headers?: Record<string, string>,
108109 ): Promise<PreparedOpenApiSpec | null> {
109110 const endpoint = resolveExternalGenerateEndpoint();
110111 if (!endpoint) {
@@ -119,6 +120,9 @@ async function loadPreparedOpenApiSpecFromExternalGenerate( · 8 unmodified lines
119120 url.searchParams.set("specUrl", specUrl);
120121 url.searchParams.set("sourceName", sourceName);
121122 url.searchParams.set("includeDts", includeDts ? "1" : "0");
123+ if (headers && Object.keys(headers).length > 0) {
124+ url.searchParams.set("headers", JSON.stringify(headers));
125+ }
122126
123127 const response = await fetch(url.toString(), {
124128 method: "GET",
@@ -353,6 +357,7 @@ export function normalizeExternalToolSource(raw: { · 228 unmodified lines
353357 sourceId: normalizedRaw.id,
354358 sourceKey: `source:${normalizedRaw.id}`,
355359 url: config.url,
360+ useCredentialedFetch: config.useCredentialedFetch,
356361 auth: config.auth,
357362 transport: config.transport,
358363 queryParams: config.queryParams,
@@ -376,6 +381,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines
376381 sourceId: normalizedRaw.id,
377382 sourceKey: `source:${normalizedRaw.id}`,
378383 endpoint: config.endpoint,
384+ useCredentialedFetch: config.useCredentialedFetch,
379385 schema: config.schema,
380386 auth: config.auth,
381387 defaultQueryApproval: config.defaultQueryApproval,
@@ -399,6 +405,7 @@ export function normalizeExternalToolSource(raw: { · 17 unmodified lines
399405 sourceId: normalizedRaw.id,
400406 sourceKey: `source:${normalizedRaw.id}`,
401407 spec: config.spec,
408+ useCredentialedFetch: config.useCredentialedFetch,
402409 collectionUrl: config.collectionUrl,
403410 postmanProxyUrl: config.postmanProxyUrl,
404411 baseUrl: config.baseUrl,
@@ -500,12 +507,91 @@ async function resolveMcpDiscoveryHeaders( · 95 unmodified lines
500507 return { headers, warnings: [] };
501508 }
502509
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+
503587 async function loadCachedOpenApiSpec(
504588 ctx: ActionCtx,
505589 specUrl: string,
506590 sourceName: string,
507591 includeDts: boolean,
592+ headers?: Record<string, string>,
508593 ): Promise<PreparedOpenApiSpec> {
594+ const hasHeaders = Boolean(headers && Object.keys(headers).length > 0);
509595 const getDtsStatus = (prepared: PreparedOpenApiSpec): "ready" | "failed" | "skipped" => {
510596 if (prepared.dtsStatus) {
511597 return prepared.dtsStatus;
@@ -513,44 +599,49 @@ async function loadCachedOpenApiSpec( · 1 unmodified line
513599 return prepared.dts ? "ready" : "failed";
514600 };
515601
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+ });
522609
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+ }
533620
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+ }
538626 }
539627 }
540628 }
629+ } catch (error) {
630+ const message = error instanceof Error ? error.message : String(error);
631+ console.warn(`[executor] OpenAPI cache read failed for '${sourceName}': ${message}`);
541632 }
542− } catch (error) {
543− const message = error instanceof Error ? error.message : String(error);
544− console.warn(`[executor] OpenAPI cache read failed for '${sourceName}': ${message}`);
545633 }
546634
547635 const externalPrepared = await loadPreparedOpenApiSpecFromExternalGenerate(
548636 specUrl,
549637 sourceName,
550638 includeDts,
639+ headers,
551640 );
552641 if (externalPrepared) {
553− await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared);
642+ if (!hasHeaders) {
643+ await cachePreparedOpenApiSpec(ctx, specUrl, sourceName, externalPrepared);
644+ }
554645 return externalPrepared;
555646 }
556647
@@ -615,14 +706,16 @@ async function loadCachedOpenApiSpec( · 58 unmodified lines
615706 throw new Error(`Prepared OpenAPI payload for '${sourceName}' was invalid`);
616707 }
617708
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+ }
626719
627720 return prepared;
628721 }
@@ -636,7 +729,15 @@ export async function loadSourceArtifact( · 7 unmodified lines
636729
637730 if (source.type === "openapi" && typeof source.spec === "string") {
638731 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;
640741 const sharedArtifactCacheKey = sharedArtifactCacheEligible
641742 ? sharedOpenApiArtifactCacheKey(source)
642743 : undefined;
@@ -652,16 +753,25 @@ export async function loadSourceArtifact( · 9 unmodified lines
652753 }
653754 }
654755
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+ );
656763 const artifact = compileOpenApiArtifactFromPrepared(source, prepared);
657764
658765 if (sharedArtifactCacheEligible && sharedArtifactCacheKey) {
659766 await putSharedOpenApiArtifactCache(ctx, sharedArtifactCacheKey, artifact);
660767 }
661768
662− const warnings = (prepared.warnings ?? []).map(
769+ const warnings = [
770+ ...resolvedOpenApiHeaders.warnings,
771+ ...(prepared.warnings ?? []).map(
663772 (warning) => `Source '${source.name}': ${warning}`,
664− );
773+ ),
774+ ];
665775 return {
666776 artifact,
667777 warnings,