Skip to content
Open
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 @@ -165,8 +165,20 @@ export class AsyncAPIConverter extends AbstractSpecConverter<AsyncAPIConverterCo

private convertSecuritySchemes(): void {
if (this.context.authOverrides) {
// When auth-schemes is present without auth, synthesize auth: { any: [...schemeNames] }
let effectiveOverrides = this.context.authOverrides;
if (this.context.authOverrides.auth == null && this.context.authOverrides["auth-schemes"] != null) {
const schemeNames = Object.keys(this.context.authOverrides["auth-schemes"]);
if (schemeNames.length > 0) {
effectiveOverrides = {
...this.context.authOverrides,
auth: { any: schemeNames }
};
}
}

const overrideAuth = convertApiAuth({
rawApiFileSchema: this.context.authOverrides,
rawApiFileSchema: effectiveOverrides,
casingsGenerator: this.context.casingsGenerator
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,23 @@ export class OpenAPIConverter extends AbstractSpecConverter<OpenAPIConverterCont
private convertSecuritySchemes(): void {
const openApiSchemes = this.convertOpenApiSecuritySchemes();
if (this.context.authOverrides) {
const descriptions = new Map(openApiSchemes.map((scheme) => [scheme.key, scheme.docs]));
this.convertAuthOverrides(descriptions, this.context.authOverrides);
return;
if (this.context.authOverrides.auth != null) {
const descriptions = new Map(openApiSchemes.map((scheme) => [scheme.key, scheme.docs]));
this.convertAuthOverrides(descriptions, this.context.authOverrides);
return;
}

if (this.context.authOverrides["auth-schemes"] != null) {
const schemeNames = Object.keys(this.context.authOverrides["auth-schemes"]);
if (schemeNames.length > 0) {
const descriptions = new Map(openApiSchemes.map((scheme) => [scheme.key, scheme.docs]));
this.convertAuthOverrides(descriptions, {
...this.context.authOverrides,
auth: { any: schemeNames }
});
return;
}
}
}

if (openApiSchemes.length > 0) {
Expand Down Expand Up @@ -393,7 +407,7 @@ export class OpenAPIConverter extends AbstractSpecConverter<OpenAPIConverterCont
}

private overrideOpenApiAuthWithGeneratorsAuth(): void {
if (!this.context.authOverrides?.["auth-schemes"]) {
if (!this.context.authOverrides?.auth || !this.context.authOverrides?.["auth-schemes"]) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix auth-schemes without explicit auth key to preserve per-endpoint security
from OpenAPI specs instead of merging all schemes into a single global group.
This ensures OAuth client-credentials and bearer auth schemes render as
distinct options in the API playground for dual-auth endpoints.
type: fix
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ async function parseApiConfigurationV2Schema({
apiSettings: generatorsYml.APIDefinitionSettings;
}): Promise<generatorsYml.APIDefinition> {
const partialConfig = {
"auth-schemes": rawConfiguration["auth-schemes"],
...apiConfiguration
...apiConfiguration,
"auth-schemes": apiConfiguration["auth-schemes"] ?? rawConfiguration["auth-schemes"]
};

if (generatorsYml.isConjureSchema(apiConfiguration.specs)) {
Expand Down
20 changes: 13 additions & 7 deletions packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,16 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace {
const documents = await this.loader.loadDocuments({ context, specs });

let authOverrides: RawSchemas.WithAuthSchema | undefined =
this.generatorsConfiguration?.api?.auth != null ? { ...this.generatorsConfiguration?.api } : undefined;
this.generatorsConfiguration?.api?.auth != null ||
this.generatorsConfiguration?.api?.["auth-schemes"] != null
? { ...this.generatorsConfiguration?.api }
Comment on lines 317 to +322
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 AsyncAPIConverter has no parallel handling for the new auth-schemes-without-auth case enabled by this PR's OSSWorkspace change, so any workspace combining AsyncAPI specs with auth-schemes-only config in generators.yml (or override files via getAuthFromOverrideFiles) will have its AsyncAPI security schemes silently wiped from the IR. Mirror the synthesis logic added to OpenAPIConverter.convertSecuritySchemes() in AsyncAPIConverter.convertSecuritySchemes() (synthesize auth: { any: schemeNames } when only auth-schemes is present), or do the synthesis once in OSSWorkspace before handing off to converters.

Extended reasoning...

What the bug is

The PR's change at OSSWorkspace.ts:317-323 now populates authOverrides whenever generators.yml has auth-schemes, even with no auth key. The same authOverrides value is forwarded to both converters: OpenAPIConverterContext3_1 and AsyncAPIConverterContext (OSSWorkspace.ts:373 and OSSWorkspace.ts:400 respectively). The PR also updates OpenAPIConverter.convertSecuritySchemes() to handle this new shape by synthesizing auth: { any: schemeNames }, but AsyncAPIConverter.convertSecuritySchemes() (packages/cli/api-importers/asyncapi-to-ir/src/AsyncAPIConverter.ts:166-189) was not given the analogous treatment.

The code path that triggers it

// AsyncAPIConverter.ts:166-189
private convertSecuritySchemes(): void {
    if (this.context.authOverrides) {           // <-- now truthy in the new case
        const overrideAuth = convertApiAuth({
            rawApiFileSchema: this.context.authOverrides,
            casingsGenerator: this.context.casingsGenerator
        });
        this.addAuthToIR({ ...overrideAuth });
        return;                                 // <-- early return skips the fallback
    }
    const asyncApiSchemes = this.convertAsyncApiSecuritySchemes();
    ...
}

convertApiAuth (packages/commons/ir-utils/src/auth/convertApiAuth.ts:19-25) short-circuits when rawApiFileSchema.auth == null, returning { requirement: All, schemes: [] }. So addAuthToIR writes empty schemes and the function returns before convertAsyncApiSecuritySchemes() can parse the AsyncAPI spec's own components.securitySchemes.

Why existing code does not prevent it

Pre-PR, the guard in OSSWorkspace was this.generatorsConfiguration?.api?.auth != null, so this exact scenario (auth-schemes only) left authOverrides undefined and AsyncAPIConverter fell through to convertAsyncApiSecuritySchemes(). The new guard auth != null || "auth-schemes" != null flips that, but only OpenAPIConverter was updated to compensate.

Impact

Any workspace combining at least one AsyncAPI spec with a generators.yml that defines auth-schemes (and no top-level auth) will have all of its AsyncAPI security wiped from the IR with no error. The same regression applies to the override-file path: getAuthFromOverrideFiles (OSSWorkspace.ts:854) was extended in this PR to return an authOverrides object containing only auth-schemes, so the same wiping triggers for AsyncAPI specs whose overrides file defines only auth-schemes. In mixed OpenAPI+AsyncAPI projects mergeIntermediateRepresentation (mergeIntermediateRepresentation.ts:23-30) takes the longer schemes array, so OpenAPI auth can mask the regression — but AsyncAPI-only projects (or AsyncAPI-namespaced sub-APIs) will ship SDKs with no auth.

Step-by-step proof

Given:

# generators.yml
api:
  specs:
    - asyncapi: ./events.yml
  auth-schemes:
    apiKey:
      header: X-Api-Key

and an events.yml AsyncAPI document with:

components:
  securitySchemes:
    apiKey:
      type: httpApiKey
      in: header
      name: X-Api-Key
  1. OSSWorkspace.getIntermediateRepresentation enters the new branch at line 319-322 because api["auth-schemes"] != null. authOverrides = { "auth-schemes": { apiKey: {...} } } (no auth key).
  2. The AsyncAPI document hits the case "asyncapi": branch at line 391-411; authOverrides is passed into AsyncAPIConverterContext at OSSWorkspace.ts:400.
  3. AsyncAPIConverter.convert() calls convertSecuritySchemes(). Line 167 if (this.context.authOverrides) is truthy.
  4. convertApiAuth({ rawApiFileSchema: { "auth-schemes": {...} } }) hits the guard at convertApiAuth.ts:19-25 because rawApiFileSchema.auth == null and returns { docs: undefined, requirement: All, schemes: [] }.
  5. addAuthToIR({ requirement: All, schemes: [] }) is called, then return at line 178 — convertAsyncApiSecuritySchemes() (which would have produced an AuthScheme for apiKey from the spec's components.securitySchemes) is never invoked.
  6. The IR ships with empty auth.schemes and the generated SDK has no auth wiring for the AsyncAPI events.

Pre-PR, step 1 would not have populated authOverrides (guard required auth != null), so step 3's if would be falsy and step 5 would correctly read the scheme from the AsyncAPI spec.

Fix

Mirror the OpenAPIConverter synthesis in AsyncAPIConverter.convertSecuritySchemes() so that when authOverrides.auth == null but authOverrides["auth-schemes"] is non-empty, it synthesizes auth: { any: Object.keys(authOverrides["auth-schemes"]) } and passes that into convertApiAuth. Alternatively, do the synthesis once in OSSWorkspace before handing authOverrides to either converter so the two stay in sync without duplication.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fcb720a. Added the same synthesis logic (auth: { any: schemeNames } when only auth-schemes is present) to AsyncAPIConverter.convertSecuritySchemes(), mirroring what OpenAPIConverter already does.

: undefined;

// Fallback: read auth/auth-schemes from the spec's overrides file if not in generators.yml
if (authOverrides == null) {
authOverrides = await getAuthFromOverrideFiles(specs);
}

const environmentOverrides =
this.generatorsConfiguration?.api?.environments != null
? { ...this.generatorsConfiguration?.api }
Expand Down Expand Up @@ -567,19 +571,21 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace {
return this.createWorkspaceWithSpecsOverride({ context }, specsOverride, settings);
}

// If auth is not in generators.yml and not in settings, try to read it from the spec's overrides files
// If auth is not in generators.yml and not in settings, try to read it from the spec's overrides files.
// When only auth-schemes is in generators.yml (no auth key), still check override files for auth.
let effectiveSettings = settings;
if (this.generatorsConfiguration?.api?.auth == null && settings?.auth == null) {
const specs = await this.getOpenAPISpecsCached({ context });
const authFromOverrides = await getAuthFromOverrideFiles(specs);
if (authFromOverrides != null) {
Comment on lines 576 to 580
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 When generators.yml declares auth-schemes (but no auth) and the spec's override file declares auth:, the new guard at OSSWorkspace.ts:574-582 skips the override-file fallback, leaving effectiveSettings.auth undefined and causing FernDefinitionConverter.buildAuthOverrides to return undefined — the override file's auth is silently dropped. Fix by only skipping the fallback when override files would re-declare auth-schemes, e.g. read the override file first and honor its auth even when auth-schemes is in generators.yml.

Extended reasoning...

The bug: The new condition in toFernWorkspace at packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts:574-578 now skips the override-file fallback when generators.yml has auth-schemes, conflating two distinct concepts: auth-schemes declares the available schemes, while auth declares which scheme(s) are used. For users who split the config — auth-schemes in generators.yml, auth in the spec override file — the override-file auth is now silently ignored.

Code path that triggers it:

  1. toFernWorkspace checks the guard at OSSWorkspace.ts:574-578. Because generatorsConfiguration.api["auth-schemes"] is set, the new condition is false and getAuthFromOverrideFiles is skipped.
  2. effectiveSettings.auth remains undefined.
  3. getDefinition calls FernDefinitionConverter.convert, which calls buildAuthOverrides(settings.auth, settings.authSchemes) with both undefined.
  4. In FernDefinitionConverter.ts:84-96: perGeneratorAuth is undefined, topLevelAuth = generatorsConfiguration.api.auth is also undefined, so effectiveAuth = undefined ?? undefined = undefined and the function returns undefined at line 94-96.
  5. Downstream convert() receives no authOverrides, and the override file's auth declaration is silently dropped.

Why other code doesn't save us: The companion change in getIntermediateRepresentation (lines 320-323) also skips getAuthFromOverrideFiles when authOverrides is non-null, and authOverrides is non-null in this scenario (because auth-schemes is in generators.yml). And buildAuthOverrides strictly requires effectiveAuth != null to return anything, so the presence of auth-schemes alone is not enough to produce overrides in the toFernWorkspace path.

Impact: Silent data loss for a legitimate split configuration. Generated SDKs will be missing the auth specified in the override file. The previous behavior (before this PR) correctly picked up auth from the override file in this scenario.

Step-by-step proof:
generators.yml:

api:
  auth-schemes:
    myBearer:
      scheme: bearer

overrides.yaml:

auth: myBearer
  • Pre-PR: guard auth == null && settings?.auth == null is true. getAuthFromOverrideFiles returns { auth: 'myBearer' }. effectiveSettings.auth = 'myBearer'. buildAuthOverrides('myBearer', undefined): effectiveAuth = 'myBearer', returns { auth: 'myBearer', 'auth-schemes': { myBearer: { scheme: bearer } } }. Auth is applied.
  • Post-PR: guard now also requires auth-schemes == null, which is false (auth-schemes is in generators.yml). Fallback skipped. effectiveSettings.auth = undefined. buildAuthOverrides(undefined, undefined): effectiveAuth = undefined, returns undefined at FernDefinitionConverter.ts:94-96. The override file's auth: myBearer is silently dropped.

Suggested fix: Don't conflate auth and auth-schemes. Either (a) read the override file first and only skip the fields that generators.yml already provides (so auth-schemes from generators.yml takes precedence over override-file auth-schemes, but override-file auth is still respected), or (b) keep the guard scoped to the auth key only and let buildAuthOverrides merge auth-schemes from both sources.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fcb720a. The guard now only requires auth == null (not auth-schemes == null) to trigger the override-file fallback. When auth-schemes is in generators.yml but the override file has auth, we now honor the override file's auth while skipping its auth-schemes (since generators.yml already defines those). This handles the split-config case correctly.

const hasAuthSchemesInGenerators = this.generatorsConfiguration?.api?.["auth-schemes"] != null;
effectiveSettings = {
...settings,
auth: authFromOverrides.auth as RawSchemas.ApiAuthSchema,
authSchemes: authFromOverrides["auth-schemes"] as Record<
string,
RawSchemas.AuthSchemeDeclarationSchema
>
// Only use override file's auth-schemes if generators.yml doesn't already define them
authSchemes: hasAuthSchemesInGenerators
? undefined
: (authFromOverrides["auth-schemes"] as Record<string, RawSchemas.AuthSchemeDeclarationSchema>)
};
}
}
Expand Down Expand Up @@ -847,7 +853,7 @@ async function getAuthFromOverrideFiles(specs: Spec[]): Promise<RawSchemas.WithA
try {
const contents = (await readFile(overridePath)).toString();
const parsed = yaml.load(contents) as Record<string, unknown> | null | undefined;
if (parsed != null && parsed["auth"] != null) {
if (parsed != null && (parsed["auth"] != null || parsed["auth-schemes"] != null)) {
return {
auth: parsed["auth"] as RawSchemas.WithAuthSchema["auth"],
...(parsed["auth-schemes"] != null
Expand Down
Loading