From da178aea40b58c0d14102581c941020b2240cc4e Mon Sep 17 00:00:00 2001 From: Ting-Hong Shieh <32212900+ting-hong-shieh@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:08:05 +0800 Subject: [PATCH 1/2] feat: allow custom FalconClient user agent --- README.md | 5 +++++ src/client.ts | 6 +++++- src/middleware/oauth2.ts | 3 ++- src/middleware/useragent.ts | 10 +++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 686fefbb..a030ba4d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ const client = new FalconClient({ cloud: "us-1", clientId: "your-client-id", clientSecret: "your-client-secret", + // Optional: identifies your integration while preserving the FalconJS version. + userAgentOverride: "example-integration/1.0.0", }); await client.sensorDownload @@ -45,6 +47,9 @@ await client.sensorDownload }); ``` +When `userAgentOverride` is set, FalconJS sends it before its own identifier. The +example above produces `example-integration/1.0.0 falconjs/0.7.0`. + ## Documentation - [Example usage under nodejs](examples/node/README.md) diff --git a/src/client.ts b/src/client.ts index 7f8d1fde..993c0ef6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -166,6 +166,9 @@ export interface FalconClientOptions { /** (optional) Member CID (MSSP targetting). Please provide memberCid only if your clientId/clientSecret key pair has access to multiple CID environments. */ memberCid?: string; + + /** (optional) Identifies a downstream integration before the FalconJS user agent. */ + userAgentOverride?: string; } export class FalconClient { @@ -323,11 +326,12 @@ export class FalconClient { clientId: options.clientId, clientSecret: options.clientSecret, memberCid: options.memberCid, + userAgentOverride: options.userAgentOverride, }); this.config = new Configuration({ fetchApi: options.fetchApi, accessToken: oauth2.accessToken.bind(oauth2), - middleware: [new UserAgent()], + middleware: [new UserAgent(options.userAgentOverride)], basePath: CloudBasePath(options.cloud), }); // @generated:assignments diff --git a/src/middleware/oauth2.ts b/src/middleware/oauth2.ts index 0584c5f3..24c80a31 100644 --- a/src/middleware/oauth2.ts +++ b/src/middleware/oauth2.ts @@ -14,6 +14,7 @@ type OAuth2Options = { clientId: string; clientSecret: string; memberCid?: string; + userAgentOverride?: string; }; export class OAuth2 { @@ -55,7 +56,7 @@ export class OAuth2 { const config = new Configuration({ basePath: CloudBasePath(this.options.cloud), fetchApi: this.options.fetchApi || fetch, - middleware: [new UserAgent()], + middleware: [new UserAgent(this.options.userAgentOverride)], }); const api = new Oauth2Api(config); const response = await api.oauth2AccessToken(this.options.clientId, this.options.clientSecret, this.options.memberCid); diff --git a/src/middleware/useragent.ts b/src/middleware/useragent.ts index ea132324..63ac4a00 100644 --- a/src/middleware/useragent.ts +++ b/src/middleware/useragent.ts @@ -1,14 +1,22 @@ import { FetchParams, RequestContext } from "../runtime"; +const DEFAULT_USER_AGENT = "falconjs/0.7.0"; + export class UserAgent { + constructor(private userAgentOverride?: string) {} + async pre(context: RequestContext): Promise { + const userAgent = this.userAgentOverride + ? `${this.userAgentOverride} ${DEFAULT_USER_AGENT}` + : DEFAULT_USER_AGENT; + return { url: context.url, init: { ...context.init, headers: { ...context.init.headers, - "User-Agent": "falconjs/0.7.0", + "User-Agent": userAgent, }, }, }; From eb930c535e71bcd1df3ec82c3a6e269a71dd1f83 Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Thu, 13 Aug 2026 12:06:36 -0400 Subject: [PATCH 2/2] validate userAgentOverride against RFC 9110 product-token grammar Reject overrides containing spaces in the wrong place, token delimiters, or CR/LF before they reach the User-Agent header. Trim surrounding whitespace so a padded value composes cleanly, and escape the offending value in the thrown error to avoid log injection. --- src/middleware/useragent.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/middleware/useragent.ts b/src/middleware/useragent.ts index 63ac4a00..9e9159b2 100644 --- a/src/middleware/useragent.ts +++ b/src/middleware/useragent.ts @@ -2,13 +2,30 @@ import { FetchParams, RequestContext } from "../runtime"; const DEFAULT_USER_AGENT = "falconjs/0.7.0"; +// RFC 9110 §5.6.2 tchar. A User-Agent product is token["/"token] (§10.1.5), +// and multiple products are separated by a single space. +const TCHAR = "[-!#$%&'*+.^_`|~0-9A-Za-z]"; +const TOKEN = `${TCHAR}+`; +const PRODUCT = `${TOKEN}(?:/${TOKEN})?`; +const PRODUCT_LIST = new RegExp(`^${PRODUCT}(?: ${PRODUCT})*$`); + export class UserAgent { - constructor(private userAgentOverride?: string) {} + private readonly userAgentOverride?: string; + + constructor(userAgentOverride?: string) { + const trimmed = userAgentOverride?.trim(); + if (trimmed) { + if (!PRODUCT_LIST.test(trimmed)) { + throw new Error( + `Invalid userAgentOverride ${JSON.stringify(userAgentOverride)}: expected one or more RFC 9110 product tokens (e.g. "my-integration/1.0.0") separated by single spaces.`, + ); + } + this.userAgentOverride = trimmed; + } + } async pre(context: RequestContext): Promise { - const userAgent = this.userAgentOverride - ? `${this.userAgentOverride} ${DEFAULT_USER_AGENT}` - : DEFAULT_USER_AGENT; + const userAgent = this.userAgentOverride ? `${this.userAgentOverride} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT; return { url: context.url,