Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: OpenApi Parser v2 auth header papercuts #2162

Merged
merged 4 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
markdownToString,
splitMarkdownIntoSections,
} from "@fern-docs/mdx";
import { createHash } from "crypto";
import { compact, flatten } from "es-toolkit/array";
import { decode } from "html-entities";
import { maybePrepareMdxContent } from "../../utils/prepare-mdx-content";
import { FernTurbopufferRecordWithoutVector } from "../types";
import { BaseRecord } from "./create-base-record";
import { createHash } from "crypto";

interface CreateMarkdownRecordsOptions {
base: BaseRecord;
Expand Down Expand Up @@ -181,7 +181,9 @@ export async function createMarkdownRecords({
return chunked_content.map((chunk, i) => {
const record: FernTurbopufferRecordWithoutVector = {
...base_markdown_record,
id: createHash("sha256").update(`${base.id}-${heading.id}-chunk:${i}`).digest("hex"),
id: createHash("sha256")
.update(`${base.id}-${heading.id}-chunk:${i}`)
.digest("hex"),
attributes: {
...base_markdown_record.attributes,
...hierarchy,
Expand Down
2 changes: 1 addition & 1 deletion packages/parsers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fern-api/docs-parsers",
"version": "0.0.55",
"version": "0.0.56",
"repository": {
"type": "git",
"url": "https://github.com/fern-api/fern-platform.git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,7 @@ export class OpenApiDocumentConverterNode extends BaseOpenApiV3_1ConverterNode<
FernRegistry.api.latest.SubpackageMetadata
> = computeSubpackages({ endpoints, webhookEndpoints });

const types = {
...this.components?.convert(),
...this.context.generatedTypes,
};
const { types, auths } = this.components?.convert() ?? {};

return {
id: FernRegistry.ApiDefinitionId(apiDefinitionId),
Expand All @@ -159,13 +156,14 @@ export class OpenApiDocumentConverterNode extends BaseOpenApiV3_1ConverterNode<
},
types:
types != null
? Object.fromEntries(
Object.entries(types).map(([id, type]) => [id, type])
)
? {
...types,
...this.context.generatedTypes,
}
: {},
// This is not necessary and will be removed
subpackages,
auths: this.auth?.convert() ?? {},
auths: { ...auths, ...(this.auth?.convert() ?? {}) },
globalHeaders: this.globalHeaders?.convert(),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ export class OAuth2SecuritySchemeConverterNode extends BaseOpenApiV3_1ConverterN
convert(): FernRegistry.api.latest.AuthScheme | undefined {
const accessTokenLocator = this.accessTokenLocatorNode?.convert();
if (accessTokenLocator == null || this.authorizationUrl == null) {
return undefined;
return {
type: "bearerAuth",
tokenName: undefined,
};
}

// TODO: revisit this -- this is not correct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ describe("OAuth2SecuritySchemeConverterNode", () => {
pathId: "test",
});

expect(node.convert()).toBeUndefined();
expect(node.convert()).toEqual({
type: "bearerAuth",
tokenName: undefined,
});
expect(mockContext.errors.error).toHaveBeenCalledWith({
message: "Expected 'tokenUrl' property to be specified",
path: ["test"],
Expand All @@ -86,7 +89,10 @@ describe("OAuth2SecuritySchemeConverterNode", () => {
pathId: "test",
});

expect(node.convert()).toBeUndefined();
expect(node.convert()).toEqual({
type: "bearerAuth",
tokenName: undefined,
});
});

it("should handle missing client credentials flow", () => {
Expand All @@ -102,6 +108,9 @@ describe("OAuth2SecuritySchemeConverterNode", () => {
pathId: "test",
});

expect(node.convert()).toBeUndefined();
expect(node.convert()).toEqual({
type: "bearerAuth",
tokenName: undefined,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ describe("SecuritySchemeConverterNode", () => {
tokenName: "TOKEN_VAR",
});
});

it("should parse bearer auth with no extension", () => {
const input = {
type: "http",
scheme: "bearer",
} as OpenAPIV3_1.HttpSecurityScheme;

const node = new SecuritySchemeConverterNode({
input,
context: mockContext,
accessPath: [],
pathId: "test",
});

const converted = node.convert();
expect(converted).toEqual({
type: "bearerAuth",
tokenName: undefined,
});
});
});

describe("header auth", () => {
Expand Down
108 changes: 86 additions & 22 deletions packages/parsers/src/openapi/3.1/schemas/ComponentsConverter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,33 @@ import {
BaseOpenApiV3_1ConverterNode,
BaseOpenApiV3_1ConverterNodeConstructorArgs,
} from "../../BaseOpenApiV3_1Converter.node";
import { resolveSecurityScheme } from "../../utils/3.1/resolveSecurityScheme";
import { maybeSingleValueToArray } from "../../utils/maybeSingleValueToArray";
import { SecuritySchemeConverterNode } from "../auth/SecuritySchemeConverter.node";
import { isReferenceObject } from "../guards/isReferenceObject";
import { SchemaConverterNode } from "./SchemaConverter.node";

export function hasOpenApiLikeSecurityScheme(
input: OpenAPIV3_1.ComponentsObject | Components
): input is OpenAPIV3_1.ComponentsObject {
return (
typeof input.securitySchemes === "object" && input.securitySchemes != null
);
}

declare namespace ComponentsConverterNode {
interface Output {
types: FernRegistry.api.latest.ApiDefinition["types"];
auths: FernRegistry.api.latest.ApiDefinition["auths"];
}
}

export class ComponentsConverterNode extends BaseOpenApiV3_1ConverterNode<
OpenAPIV3_1.ComponentsObject | Components,
FernRegistry.api.latest.ApiDefinition["types"]
ComponentsConverterNode.Output
> {
typeSchemas: Record<string, SchemaConverterNode> | undefined;
securitySchemes: Record<string, SecuritySchemeConverterNode> | undefined;

constructor(
args: BaseOpenApiV3_1ConverterNodeConstructorArgs<
Expand Down Expand Up @@ -41,34 +60,79 @@ export class ComponentsConverterNode extends BaseOpenApiV3_1ConverterNode<
})
);
}
if (
hasOpenApiLikeSecurityScheme(this.input) &&
this.input.securitySchemes != null
) {
this.securitySchemes = Object.fromEntries(
Object.entries(this.input.securitySchemes ?? {})
.map(([key, securityScheme], index) => {
let resolvedScheme: OpenAPIV3_1.SecuritySchemeObject | undefined;
if (isReferenceObject(securityScheme)) {
resolvedScheme = resolveSecurityScheme(
securityScheme.$ref,
this.context.document
);
} else {
resolvedScheme = securityScheme;
}
if (resolvedScheme == null) {
return undefined;
}
return [
key,
new SecuritySchemeConverterNode({
input: resolvedScheme,
context: this.context,
accessPath: this.accessPath,
pathId: ["securitySchemes", `${index}`],
}),
];
})
.filter(isNonNullish)
);
}
}

convert(): FernRegistry.api.latest.ApiDefinition["types"] | undefined {
convert(): ComponentsConverterNode.Output | undefined {
if (this.typeSchemas == null) {
return undefined;
}

return Object.fromEntries(
Object.entries(this.typeSchemas)
.map(([key, value]) => {
const name = value.name ?? key;
const maybeShapes = maybeSingleValueToArray(value.convert());
return {
auths: Object.fromEntries(
Object.entries(this.securitySchemes ?? {})
.map(([key, value]) => {
const maybeAuth = value.convert();
if (maybeAuth == null) {
return undefined;
}
return [FernRegistry.api.latest.AuthSchemeId(key), maybeAuth];
})
.filter(isNonNullish)
),
types: Object.fromEntries(
Object.entries(this.typeSchemas)
.map(([key, value]) => {
const name = value.name ?? key;
const maybeShapes = maybeSingleValueToArray(value.convert());

if (maybeShapes == null) {
return [key, undefined];
}
if (maybeShapes == null) {
return [key, undefined];
}

return [
FernRegistry.TypeId(key),
{
name,
shape: maybeShapes[0],
description: value.description,
availability: undefined,
},
];
})
.filter(([_, value]) => isNonNullish(value))
);
return [
FernRegistry.TypeId(key),
{
name,
shape: maybeShapes[0],
description: value.description,
availability: undefined,
},
];
})
.filter(([_, value]) => isNonNullish(value))
),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,16 @@ export class SchemaConverterNode extends BaseOpenApiV3_1ConverterNodeWithTrackin
accessPath: this.accessPath,
pathId: this.pathId,
});
this.context.errors.error({
message: "Expected type declaration. Received: null",
path: this.accessPath,
});
if (
!isReferenceObject(this.input) &&
((!isArraySchema(this.input) && this.input == null) ||
(isArraySchema(this.input) && this.input.items == null))
) {
this.context.errors.error({
message: "Expected type declaration. Received: null",
path: this.accessPath,
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe("ArrayConverterNode", () => {
]);
});

it("should return undefined if inner schema conversion fails", () => {
it("should return unknown if inner schema conversion fails", () => {
const input: ArrayConverterNode.Input = {
type: "array",
items: { type: "invalid" as OpenAPIV3_1.NonArraySchemaObjectType },
Expand All @@ -91,10 +91,6 @@ describe("ArrayConverterNode", () => {
seenSchemas: new Set(),
});
const converted = node.convert();
expect(mockContext.errors.error).toHaveBeenCalledWith({
message: "Expected type declaration. Received: null",
path: ["test", "items"],
});
expect(converted).toEqual([
{
type: "alias",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ describe("ComponentsConverterNode", () => {
accessPath: [],
pathId: "test",
});
const result = converter.convert() ?? {};
const { types } = converter.convert() ?? {};

expect(result).toBeDefined();
const firstKey = FernRegistry.TypeId(Object.keys(result)[0] ?? "");
expect(result[firstKey]?.name).toBe("User");
expect(types).toBeDefined();
const firstKey = FernRegistry.TypeId(Object.keys(types)[0] ?? "");
expect(types[firstKey]?.name).toBe("User");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1067,5 +1067,11 @@
"displayName": "voices"
}
},
"auths": {}
"auths": {
"apiKeyAuth": {
"type": "header",
"nameOverride": "apiKey",
"headerWireValue": "X_API_KEY"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"message": "Expected 'x-fern-access-token-locator' property to be specified",
"path": [
"components",
"securitySchemes",
"0",
],
},
]
Loading