From 8e1f84062bb22ed666f88f38fb5ee0eb8d75b7c8 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:19:05 -0700 Subject: [PATCH 1/2] Support persisted OpenAPI spec overrides --- .changeset/persist-openapi-spec-overrides.md | 5 + packages/plugins/openapi/src/api/group.ts | 14 +- packages/plugins/openapi/src/api/handlers.ts | 6 + packages/plugins/openapi/src/promise.ts | 1 + .../src/react/AddOpenApiIntegration.tsx | 36 +- .../openapi/src/react/SpecOverridesEditor.tsx | 50 +++ .../openapi/src/react/UpdateSpecSection.tsx | 25 +- packages/plugins/openapi/src/sdk/config.ts | 9 +- packages/plugins/openapi/src/sdk/errors.ts | 11 + packages/plugins/openapi/src/sdk/index.ts | 8 + packages/plugins/openapi/src/sdk/parse.ts | 5 +- packages/plugins/openapi/src/sdk/plugin.ts | 120 +++++- .../src/sdk/spec-overrides-lifecycle.test.ts | 164 ++++++++ .../openapi/src/sdk/spec-overrides.test.ts | 125 ++++++ .../plugins/openapi/src/sdk/spec-overrides.ts | 361 ++++++++++++++++++ 15 files changed, 920 insertions(+), 20 deletions(-) create mode 100644 .changeset/persist-openapi-spec-overrides.md create mode 100644 packages/plugins/openapi/src/react/SpecOverridesEditor.tsx create mode 100644 packages/plugins/openapi/src/sdk/spec-overrides-lifecycle.test.ts create mode 100644 packages/plugins/openapi/src/sdk/spec-overrides.test.ts create mode 100644 packages/plugins/openapi/src/sdk/spec-overrides.ts diff --git a/.changeset/persist-openapi-spec-overrides.md b/.changeset/persist-openapi-spec-overrides.md new file mode 100644 index 000000000..09dab81ea --- /dev/null +++ b/.changeset/persist-openapi-spec-overrides.md @@ -0,0 +1,5 @@ +--- +"@executor-js/plugin-openapi": patch +--- + +Apply persisted RFC 6902 overrides to OpenAPI specifications during preview, import, and refresh so upstream documents can be corrected without maintaining a fork. diff --git a/packages/plugins/openapi/src/api/group.ts b/packages/plugins/openapi/src/api/group.ts index 91f19cc9d..d1ab56254 100644 --- a/packages/plugins/openapi/src/api/group.ts +++ b/packages/plugins/openapi/src/api/group.ts @@ -9,8 +9,14 @@ import { IntegrationSlug, } from "@executor-js/sdk/shared"; -import { OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError } from "../sdk/errors"; +import { + OpenApiParseError, + OpenApiExtractionError, + OpenApiOAuthError, + OpenApiSpecOverrideError, +} from "../sdk/errors"; import { SpecPreviewSummary } from "../sdk/preview"; +import { SpecOverridesSchema } from "../sdk/spec-overrides"; // --------------------------------------------------------------------------- // Errors — the plugin-domain tagged errors flow directly to clients @@ -25,6 +31,7 @@ const DomainErrors = [ OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, + OpenApiSpecOverrideError, IntegrationAlreadyExistsError, ] as const; @@ -35,6 +42,7 @@ const UpdateSpecErrors = [ OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, + OpenApiSpecOverrideError, IntegrationNotFound, ] as const; @@ -79,11 +87,13 @@ const AddSpecPayload = Schema.Struct({ family: Schema.optional(Schema.String), healthCheck: Schema.optional(HealthCheckSpec), authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)), + specOverrides: Schema.optional(SpecOverridesSchema), }); const PreviewSpecPayload = Schema.Struct({ spec: Schema.String, specFormat: Schema.optional(Schema.String), + specOverrides: Schema.optional(SpecOverridesSchema), }); // The `configure` payload — the new/updated auth methods to merge onto the @@ -109,6 +119,7 @@ const AddSpecResponse = Schema.Struct({ // "re-fetch from the stored source URL". const UpdateSpecPayload = Schema.Struct({ spec: Schema.optional(OpenApiSpecInputPayload), + specOverrides: Schema.optional(SpecOverridesSchema), }); const UpdateSpecResponse = Schema.Struct({ @@ -138,6 +149,7 @@ const OpenApiConfigView = Schema.Struct({ baseUrl: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), + specOverrides: Schema.optional(SpecOverridesSchema), authenticationTemplate: Schema.optional(Schema.Array(AuthenticationResponse)), }); diff --git a/packages/plugins/openapi/src/api/handlers.ts b/packages/plugins/openapi/src/api/handlers.ts index 422604d7e..b4e892d42 100644 --- a/packages/plugins/openapi/src/api/handlers.ts +++ b/packages/plugins/openapi/src/api/handlers.ts @@ -47,6 +47,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope const preview = yield* ext.previewSpec({ spec: payload.spec, specFormat: payload.specFormat, + specOverrides: payload.specOverrides, }); return specPreviewSummary(preview); }), @@ -68,6 +69,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope family: payload.family, healthCheck: payload.healthCheck, authenticationTemplate: payload.authenticationTemplate, + specOverrides: payload.specOverrides, }); }), ), @@ -100,6 +102,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope baseUrl: config.baseUrl, headers: config.headers ? { ...config.headers } : undefined, queryParams: config.queryParams ? { ...config.queryParams } : undefined, + specOverrides: config.specOverrides ? [...config.specOverrides] : undefined, authenticationTemplate: config.authenticationTemplate ? [...config.authenticationTemplate] : undefined, @@ -127,6 +130,9 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope const ext = yield* OpenApiExtensionService; const result = yield* ext.updateSpec(params.slug, { ...(payload.spec !== undefined ? { spec: payload.spec } : {}), + ...(payload.specOverrides !== undefined + ? { specOverrides: payload.specOverrides } + : {}), }); return { slug: result.slug, diff --git a/packages/plugins/openapi/src/promise.ts b/packages/plugins/openapi/src/promise.ts index eec67f9e8..15fc9a72c 100644 --- a/packages/plugins/openapi/src/promise.ts +++ b/packages/plugins/openapi/src/promise.ts @@ -6,6 +6,7 @@ export type { OpenApiSpecInput, OpenApiPreviewInput, } from "./sdk/plugin"; +export type { JsonPatchOperation, SpecOverrides } from "./sdk/spec-overrides"; // Auth-template authoring helpers. Author apikey methods as canonical // placements, or request-shaped: `headers: { Authorization: ["Bearer ", diff --git a/packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx b/packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx index 730f4b8a7..7e3ce5e60 100644 --- a/packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx +++ b/packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx @@ -49,6 +49,8 @@ import type { SpecPreviewSummary } from "../sdk/preview"; import { type Authentication } from "../sdk/types"; import { resolveServerUrl } from "../sdk/openapi-utils"; import { detectedAuthenticationTemplates } from "../sdk/derive-auth"; +import { parseSpecOverridesText } from "../sdk/spec-overrides"; +import { SpecOverridesEditor } from "./SpecOverridesEditor"; const normalizePresetUrl = (url: string): string => { const trimmed = url.trim(); @@ -154,6 +156,7 @@ export default function AddOpenApiIntegration(props: { const openApiPresets = openApiPlugin?.presets; const initialPreset = openApiPresets?.find((preset) => preset.id === props.initialPreset) ?? null; const [specUrl, setSpecUrl] = useState(props.initialUrl ?? ""); + const [specOverridesText, setSpecOverridesText] = useState(""); const [analyzing, setAnalyzing] = useState(false); const [analyzeError, setAnalyzeError] = useState(null); @@ -233,6 +236,10 @@ export default function AddOpenApiIntegration(props: { : resolvedIntegrationId); const resolvedDescription = descriptionDraft ?? (preview ? Option.getOrElse(preview.description, () => "") : ""); + const parsedSpecOverrides = useMemo( + () => parseSpecOverridesText(specOverridesText), + [specOverridesText], + ); // Register EVERY spec-detected auth method, not just a single selected one. // Keyed off `preview` (stable per analysis) so the memo doesn't re-run on the @@ -333,6 +340,7 @@ export default function AddOpenApiIntegration(props: { // args filled (an empty draft, `hcOperation === ""`, imposes no constraint). const canAdd = preview !== null && + parsedSpecOverrides.ok && !slugAlreadyExists && (!previewHasNoServers || resolvedBaseUrl.length > 0) && !(hcOperation.length > 0 && hcMissingRequired); @@ -343,8 +351,19 @@ export default function AddOpenApiIntegration(props: { setAnalyzing(true); setAnalyzeError(null); setAddError(null); + if (!parsedSpecOverrides.ok) { + setAnalyzeError(parsedSpecOverrides.message); + setAnalyzing(false); + return; + } const exit = await doPreview({ - payload: { spec: specUrl, specFormat: initialPreset?.specFormat }, + payload: { + spec: specUrl, + specFormat: initialPreset?.specFormat, + ...(parsedSpecOverrides.value.length > 0 + ? { specOverrides: parsedSpecOverrides.value } + : {}), + }, }); if (Exit.isFailure(exit)) { setAnalyzeError(errorMessageFromExit(exit, "Failed to parse spec")); @@ -375,6 +394,9 @@ export default function AddOpenApiIntegration(props: { specFormat: initialPreset?.specFormat, family: initialPreset?.family, healthCheck: initialPreset?.healthCheck, + ...(parsedSpecOverrides.ok && parsedSpecOverrides.value.length > 0 + ? { specOverrides: parsedSpecOverrides.value } + : {}), // Always send the edited method list (even empty) when the user has // inspected a preview: an explicit [] means "no auth methods", while // OMITTING the field tells the server to derive defaults from the @@ -399,6 +421,7 @@ export default function AddOpenApiIntegration(props: { resolvedDescription, resolvedBaseUrl, editedAuthenticationTemplate, + parsedSpecOverrides, initialPreset?.family, initialPreset?.healthCheck, initialPreset?.specFormat, @@ -539,6 +562,17 @@ export default function AddOpenApiIntegration(props: { /> ) : null} + { + setSpecOverridesText(value); + setAnalyzeError(null); + setPreview(null); + setBaseUrl(""); + }} + disabled={analyzing || adding} + /> + {preview && ( void; + readonly disabled?: boolean; +}) { + const [open, setOpen] = useState(props.value.trim().length > 0); + const parsed = parseSpecOverridesText(props.value); + + return ( + + + Spec overrides + + +
+ JSON Patch (RFC 6902) +