From 0aae516f3a76a057cc1665fc49155d14c62c8475 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 27 Aug 2026 15:27:36 -0700 Subject: [PATCH] feat(importer-openapi): fill the OAuth redirect URI from an environment variable (#610) Co-authored-by: Claude Fable 5 --- plugins/importer-openapi/src/index.ts | 50 +++++++++++++++----- plugins/importer-openapi/tests/index.test.ts | 23 +++++++-- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/plugins/importer-openapi/src/index.ts b/plugins/importer-openapi/src/index.ts index 124aa25d..4a0e5d32 100644 --- a/plugins/importer-openapi/src/index.ts +++ b/plugins/importer-openapi/src/index.ts @@ -25,7 +25,7 @@ type ImportedAuthentication = Pick; -type OAuthVariableNames = { clientId: string; clientSecret: string }; +type OAuthVariableNames = { clientId: string; clientSecret: string; redirectUri: string }; type ServerOverrideVariable = { name: string; value: string }; const HTTP_METHODS = ["delete", "get", "head", "options", "patch", "post", "put", "query", "trace"]; @@ -172,6 +172,18 @@ export async function convertOpenApi(contents: string): Promise + model.authenticationType === "oauth2" && + Object.values(toRecord(model.authentication)).some( + (value) => typeof value === "string" && value.includes(templateVariable(redirectUri)), + ), + ); + if (used) variableNames.add(redirectUri); + } if ( authenticationConfigs.some( (model) => @@ -565,7 +577,10 @@ function importOperationName(operation: UnknownRecord, method: string, path: str * description, since a name that long is no easier to scan than the path. */ function firstLine(value: string | undefined): string | undefined { - const line = value?.split("\n").find((l) => l.trim().length > 0)?.trim(); + const line = value + ?.split("\n") + .find((l) => l.trim().length > 0) + ?.trim(); if (line == null || line.length > MAX_NAME_LENGTH) return undefined; return line; } @@ -738,8 +753,7 @@ function shouldInlinePathParameter( // so `{id}` and `{id}:cancel` stay placeholders while `report.{format}` can't const placeholderExpressible = matchingSegments.every( (segment) => - segment === template || - (segment.startsWith(template) && segment[template.length] === ":"), + segment === template || (segment.startsWith(template) && segment[template.length] === ":"), ); if (matchingSegments.length === 0 || !placeholderExpressible) return true; if (isRecord(parameter.content)) return false; @@ -1009,9 +1023,7 @@ function serializeCookieParameter(parameter: UnknownRecord, importState: ImportS if (isRecord(value)) { const entries = Object.entries(value); return explode - ? entries - .map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`) - .join("; ") + ? entries.map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`).join("; ") : `${name}=${entries.flat().map(stringifyExampleValue).join(",")}`; } return `${name}=${stringifyExampleValue(value)}`; @@ -1158,7 +1170,9 @@ function importBody({ .filter((p) => stringAt(p, "in") === "formData"); if (formParameters.length > 0) { const contentType = - toArray(operation.consumes ?? spec.consumes).find((c): c is string => typeof c === "string") ?? + toArray(operation.consumes ?? spec.consumes).find( + (c): c is string => typeof c === "string", + ) ?? (formParameters.some((p) => stringAt(p, "type") === "file") ? "multipart/form-data" : "application/x-www-form-urlencoded"); @@ -1411,7 +1425,11 @@ function mediaTypeExample(mediaType: UnknownRecord, importState: ImportState): u return schemaToExample(mediaType.schema, importState); } -function schemaToFormParameters(schema: unknown, importState: ImportState, example?: UnknownRecord) { +function schemaToFormParameters( + schema: unknown, + importState: ImportState, + example?: UnknownRecord, +) { const resolvedSchema = toRecord(importState.resolveSchema(schema)); const required = toArray(resolvedSchema.required).filter( (name): name is string => typeof name === "string", @@ -1575,7 +1593,6 @@ function coerceToDeclaredType(example: unknown, schema: UnknownRecord): unknown return example; } - function inferSchemaType(schema: UnknownRecord): string { const rawType = schema.type; if (typeof rawType === "string") return rawType; @@ -1688,6 +1705,7 @@ function importSecurityRequirement({ oauthVariablesByScheme.get(schemeName) ?? { clientId: "oauth_client_id", clientSecret: "oauth_client_secret", + redirectUri: "oauth_redirect_uri", }, useDynamicServerUrls, ); @@ -1876,9 +1894,13 @@ function importOAuth2( authorizationUrl, accessTokenUrl, clientSecret: templateVariable(variableNames.clientSecret), + redirectUri: templateVariable(variableNames.redirectUri), } : grantType === "implicit" - ? { authorizationUrl } + ? { + authorizationUrl, + redirectUri: templateVariable(variableNames.redirectUri), + } : grantType === "password" ? { accessTokenUrl, @@ -1969,7 +1991,11 @@ function buildOAuthVariablesByScheme( usedPrefixes.add(prefix); return [ schemeName, - { clientId: `${prefix}_client_id`, clientSecret: `${prefix}_client_secret` }, + { + clientId: `${prefix}_client_id`, + clientSecret: `${prefix}_client_secret`, + redirectUri: `${prefix}_redirect_uri`, + }, ]; }), ); diff --git a/plugins/importer-openapi/tests/index.test.ts b/plugins/importer-openapi/tests/index.test.ts index 1aca27f9..9416372e 100644 --- a/plugins/importer-openapi/tests/index.test.ts +++ b/plugins/importer-openapi/tests/index.test.ts @@ -384,6 +384,7 @@ describe("importer-openapi", () => { { name: "baseUrl", value: "https://api.example.com/v1" }, { name: "oauth_client_id", value: "" }, { name: "oauth_client_secret", value: "" }, + { name: "oauth_redirect_uri", value: "" }, { name: "baseUrlOrigin", value: "https://api.example.com" }, { name: "auth_api_key_key", value: "" }, ], @@ -395,6 +396,7 @@ describe("importer-openapi", () => { { name: "baseUrl", value: "https://sandbox.example.com/v1" }, { name: "oauth_client_id", value: "" }, { name: "oauth_client_secret", value: "" }, + { name: "oauth_redirect_uri", value: "" }, { name: "baseUrlOrigin", value: "https://sandbox.example.com" }, { name: "auth_api_key_key", value: "" }, ], @@ -484,6 +486,7 @@ describe("importer-openapi", () => { clientId: "${[oauth_implicitOauth_client_id]}", headerPrefix: "Bearer", authorizationUrl: "https://example.com/authorize", + redirectUri: "${[oauth_implicitOauth_redirect_uri]}", }, }), ); @@ -497,6 +500,7 @@ describe("importer-openapi", () => { { name: "oauth_oauth_client_secret", value: "" }, { name: "oauth_implicitOauth_client_id", value: "" }, { name: "oauth_implicitOauth_client_secret", value: "" }, + { name: "oauth_implicitOauth_redirect_uri", value: "" }, ], }), ); @@ -539,6 +543,7 @@ describe("importer-openapi", () => { { name: "baseUrl", value: "https://api.example.com" }, { name: "oauth_client_id", value: "" }, { name: "oauth_client_secret", value: "" }, + { name: "oauth_redirect_uri", value: "" }, ], }), ]); @@ -596,6 +601,7 @@ describe("importer-openapi", () => { { name: "baseUrl", value: "/api/v1" }, { name: "oauth_client_id", value: "" }, { name: "oauth_client_secret", value: "" }, + { name: "oauth_redirect_uri", value: "" }, { name: "baseUrlOrigin", value: "" }, ], }), @@ -633,6 +639,7 @@ describe("importer-openapi", () => { scope: "admin", authorizationUrl: "https://example.com/authorize", accessTokenUrl: "https://example.com/token", + redirectUri: "${[oauth_redirect_uri]}", }, headers: [{ enabled: true, name: "Accept", value: "application/json" }], }), @@ -1376,9 +1383,7 @@ describe("importer-openapi", () => { "application/json": { schema: { type: "object", - allOf: [ - { type: "object", properties: { fromAllOf: { example: "a" } } }, - ], + allOf: [{ type: "object", properties: { fromAllOf: { example: "a" } } }], properties: { sibling: { example: "b" } }, }, }, @@ -1398,7 +1403,17 @@ describe("importer-openapi", () => { test("Accepts unquoted YAML version numbers", async () => { const imported = await convertOpenApi( - ["swagger: 2.0", "info:", " title: Unquoted Test", ' version: "1"', "host: example.com", "paths:", " /a:", " get:", " responses: {}"].join("\n"), + [ + "swagger: 2.0", + "info:", + " title: Unquoted Test", + ' version: "1"', + "host: example.com", + "paths:", + " /a:", + " get:", + " responses: {}", + ].join("\n"), ); expect(imported?.resources.httpRequests[0]?.url).toBe("${[baseUrl]}/a");