feat(importer-openapi): fill the OAuth redirect URI from an environment variable (#610)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-27 15:27:36 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent b7b8ae5f94
commit 0aae516f3a
2 changed files with 57 additions and 16 deletions
+38 -12
View File
@@ -25,7 +25,7 @@ type ImportedAuthentication = Pick<HttpRequest, "authentication" | "authenticati
urlParameters: HttpUrlParameter[];
};
type AuthenticationVariableRegistry = Map<string, { name: string; value: string }>;
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<ImportPluginResp
clientSecret,
]),
);
// Only redirect-based grants reference the redirect variable, so don't
// create it for specs that import as client_credentials or password
for (const { redirectUri } of oauthVariablesByScheme.values()) {
const used = authenticationConfigs.some(
(model) =>
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`,
},
];
}),
);
+19 -4
View File
@@ -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");