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..9e9159b2 100644 --- a/src/middleware/useragent.ts +++ b/src/middleware/useragent.ts @@ -1,14 +1,39 @@ 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 { + 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; + return { url: context.url, init: { ...context.init, headers: { ...context.init.headers, - "User-Agent": "falconjs/0.7.0", + "User-Agent": userAgent, }, }, };