fix(cli): preserve per-endpoint security when auth-schemes defined without auth key#16037
Conversation
…thout auth key Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
|
@claude review once |
| let effectiveSettings = settings; | ||
| if (this.generatorsConfiguration?.api?.auth == null && settings?.auth == null) { | ||
| if ( | ||
| this.generatorsConfiguration?.api?.auth == null && | ||
| this.generatorsConfiguration?.api?.["auth-schemes"] == null && | ||
| settings?.auth == null | ||
| ) { | ||
| const specs = await this.getOpenAPISpecsCached({ context }); | ||
| const authFromOverrides = await getAuthFromOverrideFiles(specs); | ||
| if (authFromOverrides != null) { |
There was a problem hiding this comment.
🔴 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:
toFernWorkspacechecks the guard atOSSWorkspace.ts:574-578. BecausegeneratorsConfiguration.api["auth-schemes"]is set, the new condition is false andgetAuthFromOverrideFilesis skipped.effectiveSettings.authremainsundefined.getDefinitioncallsFernDefinitionConverter.convert, which callsbuildAuthOverrides(settings.auth, settings.authSchemes)with bothundefined.- In
FernDefinitionConverter.ts:84-96:perGeneratorAuthisundefined,topLevelAuth = generatorsConfiguration.api.authis alsoundefined, soeffectiveAuth = undefined ?? undefined = undefinedand the function returnsundefinedat line 94-96. - Downstream
convert()receives noauthOverrides, and the override file'sauthdeclaration 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: beareroverrides.yaml:
auth: myBearer- Pre-PR: guard
auth == null && settings?.auth == nullis true.getAuthFromOverrideFilesreturns{ 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-schemesis ingenerators.yml). Fallback skipped.effectiveSettings.auth = undefined.buildAuthOverrides(undefined, undefined):effectiveAuth = undefined, returnsundefinedatFernDefinitionConverter.ts:94-96. The override file'sauth: myBeareris 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.
There was a problem hiding this comment.
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 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 } |
There was a problem hiding this comment.
🔴 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-Keyand an events.yml AsyncAPI document with:
components:
securitySchemes:
apiKey:
type: httpApiKey
in: header
name: X-Api-KeyOSSWorkspace.getIntermediateRepresentationenters the new branch at line 319-322 becauseapi["auth-schemes"] != null.authOverrides = { "auth-schemes": { apiKey: {...} } }(noauthkey).- The AsyncAPI document hits the
case "asyncapi":branch at line 391-411;authOverridesis passed intoAsyncAPIConverterContextatOSSWorkspace.ts:400. AsyncAPIConverter.convert()callsconvertSecuritySchemes(). Line 167if (this.context.authOverrides)is truthy.convertApiAuth({ rawApiFileSchema: { "auth-schemes": {...} } })hits the guard atconvertApiAuth.ts:19-25becauserawApiFileSchema.auth == nulland returns{ docs: undefined, requirement: All, schemes: [] }.addAuthToIR({ requirement: All, schemes: [] })is called, thenreturnat line 178 —convertAsyncApiSecuritySchemes()(which would have produced anAuthSchemeforapiKeyfrom the spec'scomponents.securitySchemes) is never invoked.- The IR ships with empty
auth.schemesand 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.
There was a problem hiding this comment.
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.
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
… AsyncAPI parallel handling Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
…logging Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
Description
When
generators.ymldefinesauth-schemes(e.g., OAuth client-credentials + bearer) but no explicitauthkey, the CLI was merging all auth schemes into a single global security group and stripping per-endpoint security from the OpenAPI spec. This caused all endpoints to appear with the same auth options regardless of their actual security requirements.Related: fern-api/fern-platform#11327 (rendering fix for the docs playground)
Changes Made
OpenAPIConverter.tsoverrideOpenApiAuthWithGeneratorsAuth(): Changed guard to require bothauthANDauth-schemesbefore overriding OpenAPI security. When onlyauth-schemesis present (noauthkey), per-endpoint security from the OpenAPI spec is now preserved.convertSecuritySchemes(): Whenauth-schemesis present withoutauth, synthesizesauth: { any: [...schemeNames] }so thatconvertAuthOverridescorrectly registers scheme types (OAuth client-credentials vs bearer) from the fern definitions.OSSWorkspace.tsauthOverrideswhenauth-schemesis present (not justauth), so the converter receives the scheme type definitions.getAuthFromOverrideFiles()to also check forauth-schemesin override files.toFernWorkspace()to skip reading from override files whenauth-schemesis already in the generators config.Testing
Verified by generating IR from a Dropbox-style config with:
Before fix: All endpoints had
[{"dropboxAppAuth": [], "dropboxOAuth2": [], "dropboxTeamAuth": []}](one merged group)After fix:
List Folder(dual-auth): 2 groups —["dropboxAppAuth"],["dropboxOAuth2"]Get Photo(single-auth): 1 group —["dropboxOAuth2"]Top-level:
dropboxAppAuth= oauth (clientCredentials),dropboxOAuth2= bearer,dropboxTeamAuth= bearerManual testing completed
Unit tests added/updated
Link to Devin session: https://app.devin.ai/sessions/ca3d4110d2874227ae4ca857e41dcc01