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 @@
1/// <reference types="next" />1/// <reference types="next" />
2/// <reference types="next/image-types/global" />2/// <reference types="next/image-types/global" />
3import "./.next/types/routes.d.ts";3import "./.next/dev/types/routes.d.ts";
4 4
5// NOTE: This file should not be edited5// 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.
executor/apps/web/src/app/api/generate/route.ts+4−14
@@ -1,9 +1,7 @@
1import { parse as parseYaml } from "yaml";1import { parse as parseYaml } from "yaml";
2import { z } from "zod";2import { z } from "zod";
3import { unstable_cache } from "next/cache";
4import { prepareOpenApiSpec } from "@executor/core/openapi-prepare";3import { prepareOpenApiSpec } from "@executor/core/openapi-prepare";
5 4
6const GENERATED_SPEC_REVALIDATE_SECONDS = 60 * 60;
7const HEADER_FETCH_TIMEOUT_MS = 12_000;5const HEADER_FETCH_TIMEOUT_MS = 12_000;
8 6
9const querySchema = z.object({7const querySchema = z.object({
@@ -26,17 +24,6 @@ const blockedForwardedHeaderNames = new Set([ · 16 unmodified lines
26 "referer",24 "referer",
27]);25]);
28 26
29const 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
40function jsonResponse(27function 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 === 0161 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,
executor/apps/web/src/components/tools/add/source-dialog.tsx+2−0
@@ -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,
executor/apps/web/src/components/tools/add/source-submit.ts+2−0
@@ -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 });
executor/apps/web/src/components/tools/add/source/auth-panel.tsx+11−13
@@ -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 <input177 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">
executor/apps/web/src/components/tools/add/source/dialog-helpers.ts+5−0
@@ -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 ...(accountId65 ...(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 };
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
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
executor/apps/web/src/components/tools/explorer/source-form-panel.tsx+2−0
@@ -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,
executor/packages/core/src/tool/source-types.ts+3−0
@@ -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;
executor/packages/database/convex/database/credentials.ts+35−0
@@ -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.db432 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
13 13
14export type NormalizedMcpToolSourceConfig = Pick<14export 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
19export type NormalizedGraphqlToolSourceConfig = Pick<19export 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
24export type NormalizedOpenApiToolSourceConfig = Pick<24export 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
29export type NormalizedToolSourceConfig =29export 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),
executor/packages/database/src/runtime/tool_source_loading.ts+146−36
@@ -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
510async 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
503async function loadCachedOpenApiSpec(587async 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 = sharedArtifactCacheEligible741 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,