From 2d473b204fc047c52345b3c96210e353a92193b7 Mon Sep 17 00:00:00 2001 From: Wagner Costa Date: Wed, 7 Oct 2026 13:13:30 -0300 Subject: [PATCH] fix: send asknews-cli/ as User-Agent --- src/cli.ts | 4 +-- src/lib/http.ts | 3 ++ src/lib/version.ts | 7 +++++ test/user-agent.test.ts | 68 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/lib/version.ts create mode 100644 test/user-agent.test.ts diff --git a/src/cli.ts b/src/cli.ts index 3b54fba..e16df06 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; import { Command, Option } from "commander"; -import packageJson from "../package.json" with { type: "json" }; import { registerApiCommands } from "./commands/api.js"; import { registerAuthCommands } from "./commands/auth.js"; import { registerCuratedCommands } from "./commands/curated.js"; @@ -8,12 +7,13 @@ import { registerSkillCommands } from "./commands/skills.js"; import { bannerEnabled, renderBanner } from "./lib/banner.js"; import { manifest } from "./lib/operations.js"; import { OUTPUT_FORMATS } from "./lib/types.js"; +import { CLI_VERSION } from "./lib/version.js"; export function buildCli(): Command { const program = new Command() .name("asknews") .description("The official AskNews CLI for humans, automation, and agents") - .version(packageJson.version) + .version(CLI_VERSION) .showHelpAfterError() .showSuggestionAfterError() // Put a blank line between each argument/option/command entry so dense, multi-line parameter diff --git a/src/lib/http.ts b/src/lib/http.ts index fac56b8..ad42059 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -4,6 +4,7 @@ import type { CliConfig } from "./config.js"; import { ApiError, NetworkError, UsageError } from "./errors.js"; import { redact } from "./redact.js"; import type { ApiResponse, OperationDefinition, RequestInput, TokenRefresher } from "./types.js"; +import { USER_AGENT } from "./version.js"; // Default timeouts for operations whose normal completion time exceeds the global 60 s // default. Applied only when the user did not set --timeout or ASKNEWS_TIMEOUT_MS. @@ -90,6 +91,7 @@ export async function executeRawRequest( const headers = new Headers({ accept: "application/json", authorization: `Bearer ${credential}`, + "user-agent": USER_AGENT, }); return executeRequest( config, @@ -191,6 +193,7 @@ function prepareOperationRequest( const headers = new Headers({ accept: input.body && isStreamBody(input.body) ? "text/event-stream" : "application/json", authorization: `Bearer ${credential}`, + "user-agent": USER_AGENT, ...input.headers, }); let body: string | undefined; diff --git a/src/lib/version.ts b/src/lib/version.ts new file mode 100644 index 0000000..192522f --- /dev/null +++ b/src/lib/version.ts @@ -0,0 +1,7 @@ +import packageJson from "../../package.json" with { type: "json" }; + +export const CLI_VERSION: string = packageJson.version; + +// Identifies CLI traffic at the API gateway (usage is attributed per channel), next to the SDKs' +// own `asknews-sdk-` agents. Never include anything user-specific here. +export const USER_AGENT = `asknews-cli/${CLI_VERSION}`; diff --git a/test/user-agent.test.ts b/test/user-agent.test.ts new file mode 100644 index 0000000..12ec335 --- /dev/null +++ b/test/user-agent.test.ts @@ -0,0 +1,68 @@ +import { afterEach, expect, test, vi } from "vitest"; +import type { CliConfig } from "../src/lib/config.js"; +import { executeOperation, executeOperationStream, executeRawRequest } from "../src/lib/http.js"; +import type { OperationDefinition } from "../src/lib/types.js"; +import { CLI_VERSION, USER_AGENT } from "../src/lib/version.js"; + +const config: CliConfig = { + apiUrl: "http://api.invalid/v1", + authUrl: "http://auth.invalid", + oauthClientId: "test", + output: "json", + configDir: "/tmp/asknews-cli-user-agent-test", + timeoutMs: 1_000, + noColor: true, +}; + +const operation: OperationDefinition = { + operationId: "search_news", + tag: "news", + command: "news search", + method: "GET", + path: "/v1/news/search", + summary: "", + parameters: [], + requestBody: null, + safety: "read-only", +}; + +function stubFetch(): ReturnType { + const mockFetch = vi.fn( + async () => + new Response("{}", { status: 200, headers: { "content-type": "application/json" } }), + ); + vi.stubGlobal("fetch", mockFetch); + return mockFetch; +} + +function sentUserAgent(mockFetch: ReturnType): string | null { + const init = mockFetch.mock.calls[0]?.[1] as RequestInit; + return new Headers(init.headers).get("user-agent"); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("the user agent names the CLI and its package version", () => { + expect(USER_AGENT).toBe(`asknews-cli/${CLI_VERSION}`); + expect(CLI_VERSION).toMatch(/^\d+\.\d+\.\d+/); +}); + +test("raw requests send the CLI user agent", async () => { + const mockFetch = stubFetch(); + await executeRawRequest(config, "key", "GET", "/test"); + expect(sentUserAgent(mockFetch)).toBe(USER_AGENT); +}); + +test("operation requests send the CLI user agent", async () => { + const mockFetch = stubFetch(); + await executeOperation(config, "key", operation, {}); + expect(sentUserAgent(mockFetch)).toBe(USER_AGENT); +}); + +test("streamed operation requests send the CLI user agent", async () => { + const mockFetch = stubFetch(); + await executeOperationStream(config, "key", operation, { body: { stream: true } }, () => {}); + expect(sentUserAgent(mockFetch)).toBe(USER_AGENT); +});