From 436d3ed075b8c05094362a4ca4aad84ea1e881c6 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 13 May 2026 12:47:15 +0200 Subject: [PATCH 1/3] fix(openapi3): don't emit auth scheme models under components.schemas A custom auth scheme model declared inside the service namespace (`model X { type: AuthType.http; scheme: "..."; }`) was emitted both under `components.securitySchemes.X` (correct) and under `components.schemas.X` (spurious). The second emit is the side effect of `processUnreferencedSchemas`, which walks every model in the service namespace and emits any whose visibility is "unreachable from payloads". Custom auth models always match that criterion but are already represented under `securitySchemes`. The visible symptom was that any `@extension(...)` on the auth model propagated to both emit locations. Extensions valid at `components.securitySchemes.X` (e.g. `bearerFormat`) are unknown at `components.schemas.X`, and downstream OpenAPI validators (such as openapi-generator-cli) reject the document. Filter the auth scheme models out of `processUnreferencedSchemas` so they only land in `components.securitySchemes`. --- ...napi3-auth-model-schemas-leak-2026-5-13.md | 7 +++++ packages/openapi3/src/openapi.ts | 6 ++++ packages/openapi3/test/security.test.ts | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 .chronus/changes/fix-openapi3-auth-model-schemas-leak-2026-5-13.md diff --git a/.chronus/changes/fix-openapi3-auth-model-schemas-leak-2026-5-13.md b/.chronus/changes/fix-openapi3-auth-model-schemas-leak-2026-5-13.md new file mode 100644 index 00000000000..86822268a23 --- /dev/null +++ b/.chronus/changes/fix-openapi3-auth-model-schemas-leak-2026-5-13.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Fix custom auth scheme models leaking into `components.schemas` when declared inside the service namespace. They are now emitted only under `components.securitySchemes` as expected. diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index c2496d88f45..729cd2c644d 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1780,10 +1780,16 @@ function createOAPIEmitter( } function processUnreferencedSchemas() { + const authSchemeModels = new Set(serviceAuth.schemes.map((s) => s.model)); const addSchema = (type: Type) => { if (isOrExtendsHttpFile(program, type)) { return; } + if (authSchemeModels.has(type)) { + // Auth scheme models are emitted under components.securitySchemes + // and should not also appear as payload schemas in components.schemas. + return; + } if ( visibilityUsage.isUnreachable(type) && !paramModels.has(type) && diff --git a/packages/openapi3/test/security.test.ts b/packages/openapi3/test/security.test.ts index b4c6ebfe37b..ac692c5b337 100644 --- a/packages/openapi3/test/security.test.ts +++ b/packages/openapi3/test/security.test.ts @@ -589,4 +589,35 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => { }, ]); }); + + it("does not emit a custom auth scheme model under components.schemas", async () => { + // A custom auth scheme model declared inside the service namespace + // belongs only in `components.securitySchemes`. Previously + // `processUnreferencedSchemas` also emitted it under + // `components.schemas` because no payload references it, which + // caused downstream validators to reject auth-only attributes + // (e.g. `bearerFormat`) that propagated to the schemas-side copy. + const res = await openApiFor( + ` + @useAuth(customBearer) + @service + namespace MyService; + + model customBearer { + type: AuthType.http; + scheme: "bearer"; + } + + @route("/ping") + op ping(): { @statusCode _: 200; ok: boolean }; + `, + ); + deepStrictEqual(res.components.securitySchemes, { + customBearer: { + type: "http", + scheme: "bearer", + }, + }); + expect(res.components.schemas?.customBearer).toBeUndefined(); + }); }); From 93fc4a2f19aa8c5bc029d0984a9b0a0b68cfadaa Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 14 May 2026 15:41:48 +0200 Subject: [PATCH 2/3] test(openapi3): keep auth scheme model in components.schemas when referenced by an operation --- packages/openapi3/test/security.test.ts | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/openapi3/test/security.test.ts b/packages/openapi3/test/security.test.ts index ac692c5b337..b76da8a1079 100644 --- a/packages/openapi3/test/security.test.ts +++ b/packages/openapi3/test/security.test.ts @@ -620,4 +620,38 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => { }); expect(res.components.schemas?.customBearer).toBeUndefined(); }); + + it("still emits an auth scheme model under components.schemas if referenced by an operation", async () => { + // The filter in `processUnreferencedSchemas` only skips auth scheme + // models that are otherwise unreachable. If the same model is also + // referenced from a payload (e.g. returned by an operation), it must + // continue to appear under `components.schemas` so the operation can + // $ref it, while still being emitted under `components.securitySchemes`. + const res = await openApiFor( + ` + @useAuth(customBearer) + @service + namespace MyService; + + model customBearer { + type: AuthType.http; + scheme: "bearer"; + } + + @route("/echo") + op echo(): customBearer; + `, + ); + deepStrictEqual(res.components.securitySchemes, { + customBearer: { + type: "http", + scheme: "bearer", + }, + }); + expect(res.components.schemas?.customBearer).toBeDefined(); + deepStrictEqual( + res.paths["/echo"]["get"].responses["200"].content["application/json"].schema, + { $ref: "#/components/schemas/customBearer" }, + ); + }); }); From 2c2c513df440c49b8f83198b02a6b10a857a36cc Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 14 May 2026 19:29:46 +0200 Subject: [PATCH 3/3] style: format security test per prettier --- packages/openapi3/test/security.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/openapi3/test/security.test.ts b/packages/openapi3/test/security.test.ts index b76da8a1079..0380635afa3 100644 --- a/packages/openapi3/test/security.test.ts +++ b/packages/openapi3/test/security.test.ts @@ -649,9 +649,8 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => { }, }); expect(res.components.schemas?.customBearer).toBeDefined(); - deepStrictEqual( - res.paths["/echo"]["get"].responses["200"].content["application/json"].schema, - { $ref: "#/components/schemas/customBearer" }, - ); + deepStrictEqual(res.paths["/echo"]["get"].responses["200"].content["application/json"].schema, { + $ref: "#/components/schemas/customBearer", + }); }); });