diff --git a/.changeset/optional-anonymous-security.md b/.changeset/optional-anonymous-security.md new file mode 100644 index 000000000..ffbc633ed --- /dev/null +++ b/.changeset/optional-anonymous-security.md @@ -0,0 +1,7 @@ +--- +'fets': patch +--- + +Make client auth params optional when an operation's `security` array includes an anonymous alternative (`{}`) + +OpenAPI allows listing an empty security requirement beside authenticated schemes so callers may omit credentials. The client types previously still required auth params whenever any scheme was present. Empty `{}` entries are now detected and the corresponding auth params become optional. diff --git a/packages/fets/src/client/types.ts b/packages/fets/src/client/types.ts index 1af68c972..819be870c 100644 --- a/packages/fets/src/client/types.ts +++ b/packages/fets/src/client/types.ts @@ -681,12 +681,25 @@ export type SecuritySchemeName T['security'] >[number]; +type HasAnonymousSecurityAlternative = + true extends ( + T['security'][number] extends infer TRequirement + ? TRequirement extends unknown + ? keyof TRequirement extends never + ? true + : false + : false + : false + ) + ? true + : false; + export type OASSecurityParams = BasicAuthParams & BearerAuthParams & ApiKeyAuthParams & OAuth2AuthParams; -export type OASSecurityParamsBySecurityRef = TSecurityObj extends { +type OASSecurityParamsBySecurityRefBase = TSecurityObj extends { security: { [key: string]: any }[]; } ? TOAS extends @@ -729,3 +742,11 @@ export type OASSecurityParamsBySecurityRef = TSecurityObj ex }> : {} : {}; + +export type OASSecurityParamsBySecurityRef = TSecurityObj extends { + security: { [key: string]: any }[]; +} + ? HasAnonymousSecurityAlternative extends true + ? DeepPartial> + : OASSecurityParamsBySecurityRefBase + : {}; diff --git a/packages/fets/tests/client/fixtures/example-optional-apiKey-header-oas.ts b/packages/fets/tests/client/fixtures/example-optional-apiKey-header-oas.ts new file mode 100644 index 000000000..d01526b7f --- /dev/null +++ b/packages/fets/tests/client/fixtures/example-optional-apiKey-header-oas.ts @@ -0,0 +1,74 @@ +export default { + components: { + securitySchemes: { + apiKey: { + type: 'apiKey', + name: 'x-api-key', + in: 'header', + }, + }, + schemas: { + UnauthorizedResponse: { + properties: { + message: { + type: 'string', + }, + }, + type: 'object', + additionalProperties: false, + required: ['message'], + }, + User: { + properties: { + id: { + type: 'integer', + }, + name: { + type: 'string', + }, + }, + type: 'object', + additionalProperties: false, + required: ['id', 'name'], + }, + }, + }, + paths: { + '/me': { + get: { + operationId: 'getMe', + // Empty object = anonymous alternative; auth is optional + security: [ + {}, + { + apiKey: [], + }, + ], + responses: { + '200': { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/User', + }, + }, + }, + description: 'OK', + }, + 401: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/UnauthorizedResponse', + }, + }, + }, + description: 'Unauthorized', + }, + }, + summary: 'get me', + tags: ['User'], + }, + }, + }, +} as const; diff --git a/packages/fets/tests/client/optional-apiKey-test.ts b/packages/fets/tests/client/optional-apiKey-test.ts new file mode 100644 index 000000000..317a8c8e8 --- /dev/null +++ b/packages/fets/tests/client/optional-apiKey-test.ts @@ -0,0 +1,22 @@ +import { createClient, type NormalizeOAS } from '../../src'; + +type NormalizedOAS = NormalizeOAS< + (typeof import('./fixtures/example-optional-apiKey-header-oas'))['default'] +>; +const client = createClient({}); + +// Auth is optional because security includes an anonymous `{}` alternative +await client['/me'].get(); + +const res = await client['/me'].get({ + headers: { + 'x-api-key': '123', + }, +}); + +if (!res.ok) { + const errData = await res.json(); + throw new Error(errData.message); +} +const data = await res.json(); +console.info(`User ${data.id}: ${data.name}`);