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
11 changes: 10 additions & 1 deletion packages/openapi/src/gen-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,16 @@ function deriveAuth(
expr: `basic({ user: env('API_USER'), pass: env('API_PASSWORD') })`,
imports: ['basic', 'env'],
};
if (scheme.type === 'apiKey') {
if (
scheme.type === 'apiKey' &&
(scheme.in === 'header' ||
scheme.in === 'query' ||
scheme.in === undefined)
) {
// Core's `apiKey()` only has header (default) and query arms — `name` locates the key in
// both. Header is the default, so `in: 'header'` is never emitted; a query scheme gets the
// `in: 'query'` discriminant. An `in: 'cookie'` scheme has no arm, so it must NOT land here
// (it would silently become a header key) — it falls through to the not-auto-mapped warning.
const where = scheme.in === 'query' ? `in: 'query', ` : '';
const nm = scheme.name ? `name: ${JSON.stringify(scheme.name)}, ` : '';
return {
Expand Down
40 changes: 40 additions & 0 deletions packages/openapi/test/gen-openapi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,46 @@ describe('planGen — naming, typing, auth, notice', () => {
/runtime validation \+ drift are OFF/,
);
});

// apiKey auth. Core's `apiKey()` only models a header (default) or query key; a `cookie` apiKey
// has no arm. The generator must emit the header/query forms but let `cookie` fall through to
// the not-auto-mapped warning — never silently emit it as a header key.
const apiKeyDoc = (loc: 'header' | 'query' | 'cookie'): OpenApiDoc => ({
openapi: '3.0.0',
info: { title: 'T', version: '1' },
servers: [{ url: 'https://api.example.com' }],
security: [{ apiKeyAuth: [] }],
components: {
securitySchemes: {
apiKeyAuth: { type: 'apiKey', in: loc, name: 'X-API-Key' },
},
},
paths: { '/ping': { get: { operationId: 'ping', responses: {} } } },
});

test('apiKey in header → header key (no `in:`), no warning', () => {
const r = planGen(apiKeyDoc('header'), { all: true });
const c = file(r, 'client.ts') ?? '';
expect(c).toMatch(
/auth: apiKey\(\{ name: "X-API-Key", value: env\('API_KEY'\) \}\)/,
);
expect(c).not.toMatch(/in: 'query'/);
expect(r.warnings.join('\n')).not.toMatch(/not auto-mapped/);
});

test("apiKey in query → `in: 'query'` discriminant emitted", () => {
const r = planGen(apiKeyDoc('query'), { all: true });
expect(file(r, 'client.ts') ?? '').toMatch(
/auth: apiKey\(\{ in: 'query', name: "X-API-Key", value: env\('API_KEY'\) \}\)/,
);
});

test('apiKey in cookie → not auto-mapped, no silent header key', () => {
const r = planGen(apiKeyDoc('cookie'), { all: true });
// Must NOT emit an apiKey() call at all — cookie has no core arm.
expect(file(r, 'client.ts') ?? '').not.toMatch(/apiKey\(/);
expect(r.warnings.join('\n')).toMatch(/not auto-mapped/);
});
});

// The codegen turns an UNTRUSTED OpenAPI document into TS source the developer compiles. Spec text
Expand Down
Loading