diff --git a/scripts/generate-client.ts b/scripts/generate-client.ts index e2b4238..d8c7ccb 100644 --- a/scripts/generate-client.ts +++ b/scripts/generate-client.ts @@ -190,7 +190,8 @@ function generateClientCode(namespaces: Map): string { allFunctions.sort(); // Generate imports - const imports = `import { + const imports = `import packageJson from '../package.json'; +import { client, ${allFunctions.map(f => ` ${f},`).join('\n')} } from './generated/sdk.gen'; @@ -332,19 +333,21 @@ ${namespaceProps.join('\n\n')} this.baseURL = options.baseURL ?? 'https://zernio.com/api'; this._options = options; - // Configure the generated client + // Configure the generated client. User-Agent and defaultHeaders are + // applied at config time (not via the interceptor) because Node 20's + // undici treats User-Agent as a forbidden header on already-constructed + // Request objects, silently dropping \`headers.set('User-Agent', …)\`. client.setConfig({ baseUrl: this.baseURL, + headers: { + 'User-Agent': \`zernio-node/\${packageJson.version}\`, + ...(options.defaultHeaders ?? {}), + }, }); // Add auth interceptor client.interceptors.request.use((request) => { request.headers.set('Authorization', \`Bearer \${this.apiKey}\`); - if (options.defaultHeaders) { - for (const [key, value] of Object.entries(options.defaultHeaders)) { - request.headers.set(key, value); - } - } return request; }); diff --git a/src/client.ts b/src/client.ts index 4b3d6d5..43564e9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,3 +1,4 @@ +import packageJson from '../package.json'; import { client, activateSequence, @@ -965,19 +966,21 @@ export class Zernio { this.baseURL = options.baseURL ?? 'https://zernio.com/api'; this._options = options; - // Configure the generated client + // Configure the generated client. User-Agent and defaultHeaders are + // applied at config time (not via the interceptor) because Node 20's + // undici treats User-Agent as a forbidden header on already-constructed + // Request objects, silently dropping `headers.set('User-Agent', …)`. client.setConfig({ baseUrl: this.baseURL, + headers: { + 'User-Agent': `zernio-node/${packageJson.version}`, + ...(options.defaultHeaders ?? {}), + }, }); // Add auth interceptor client.interceptors.request.use((request) => { request.headers.set('Authorization', `Bearer ${this.apiKey}`); - if (options.defaultHeaders) { - for (const [key, value] of Object.entries(options.defaultHeaders)) { - request.headers.set(key, value); - } - } return request; }); diff --git a/tests/client.test.ts b/tests/client.test.ts index b57873d..c338ef6 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import Zernio, { Late, ZernioApiError, LateApiError } from '../src'; +import packageJson from '../package.json'; describe('Zernio Client', () => { it('should throw error when no API key is provided', () => { @@ -72,6 +73,45 @@ describe('Zernio Client', () => { expect(client.baseURL).toBe('https://custom.example.com/api'); }); + it('should include the SDK version in the user agent', async () => { + const client = new Zernio({ apiKey: 'test_key' }); + let request: Request | undefined; + + await client.posts.listPosts({ + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input); + return new Response('{}', { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }, + }); + + expect(request?.headers.get('User-Agent')).toBe(`zernio-node/${packageJson.version}`); + }); + + it('should allow the user agent to be overridden', async () => { + const client = new Zernio({ + apiKey: 'test_key', + defaultHeaders: { + 'User-Agent': 'custom-agent', + }, + }); + let request: Request | undefined; + + await client.posts.listPosts({ + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input); + return new Response('{}', { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }, + }); + + expect(request?.headers.get('User-Agent')).toBe('custom-agent'); + }); + it('should have all resource namespaces', () => { const client = new Zernio({ apiKey: 'test_key' });