Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@envoy/envoy-integrations-sdk",
"version": "2.4.4",
"version": "2.5.0",
"description": "SDK for building Envoy integrations.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
63 changes: 62 additions & 1 deletion src/base/EnvoyAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,28 @@ import JSONAPIData from '../util/json-api/JSONAPIData';
import { envoyBaseURL } from '../constants';
import { createAxiosClient } from '../util/axiosConstructor';
import { EMPTY_STORAGE_ERROR_MESSAGE } from '../util/errorHandling';
import { buildUserAgent, buildClientInfoHeader } from '../util/userAgent';

interface EnvoyWebDataLoaderKey extends JSONAPIData {
include?: string;
}

/**
* Options for configuring EnvoyAPI client
*/
export interface EnvoyAPIOptions {
/** Access token for authentication */
accessToken: string;
/**
* Custom application identifier appended to User-Agent header.
* Format: "AppName/Version"
* Example: "MyCompanyApp/1.0.0"
*
* This identifier helps track API usage by application and aids in debugging.
*/
userAgent?: string;
}

/**
* Sometimes envoy-web will give us back some relationship data
* with the "type" set to the relationships name instead of the actual model's name.
Expand All @@ -27,6 +44,7 @@ const TYPE_ALIASES = new Map<string, string>([['employee-screening-flows', 'flow
export default class EnvoyAPI {
/**
* HTTP Client with Envoy's defaults.
* User-Agent headers are set in the constructor after client instantiation.
*/
readonly axios = createAxiosClient({
baseURL: envoyBaseURL,
Expand Down Expand Up @@ -59,8 +77,51 @@ export default class EnvoyAPI {
},
);

constructor(accessToken: string) {
/**
* Create an EnvoyAPI client instance
*
* @param options - Either an access token string or an EnvoyAPIOptions object
* with accessToken and optional userAgent
*
* @example
* // Simple usage with access token only
* const client = new EnvoyAPI('access-token-here');
*
* @example
* // Usage with custom User-Agent for tracking and debugging
* const client = new EnvoyAPI({
* accessToken: 'access-token-here',
* userAgent: 'MyApp/1.0.0'
* });
*/
constructor(options: EnvoyAPIOptions | string) {
// Support both string and options object formats
const { accessToken, userAgent } = typeof options === 'string'
? { accessToken: options, userAgent: undefined }
: options;

// Set authorization header (critical - must succeed)
this.axios.defaults.headers.authorization = `Bearer ${accessToken}`;

// Set User-Agent headers with absolute guarantee that failures won't break SDK
// GUARANTEE: This block will NEVER throw an exception, no matter what happens
// User-Agent headers are telemetry/debugging aids, not critical for SDK functionality
try {
// Primary attempt: Use full header generation functions
this.axios.defaults.headers['User-Agent'] = buildUserAgent(userAgent);
this.axios.defaults.headers['X-Envoy-Client-Info'] = buildClientInfoHeader(userAgent);
} catch (error) {
// Secondary fallback: Set minimal valid headers
try {
this.axios.defaults.headers['User-Agent'] = 'envoy-integrations-sdk/unknown';
this.axios.defaults.headers['X-Envoy-Client-Info'] = '{"sdk":"envoy-integrations-sdk"}';
} catch (fallbackError) {
// Tertiary fallback: Even setting minimal headers failed
// Continue without User-Agent headers - SDK remains fully functional
// This catch ensures absolute guarantee that SDK initialization succeeds
}
}

/**
* Saves every model that was "include"ed in the response,
* which saves us the trouble of fetching related data.
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export * from './util/EnvoySignatureVerifier';
export * from './util/axiosConstructor';
export * from './util/errorHandling';

export type { EnvoyAPIOptions } from './base/EnvoyAPI';

export {
EntryPayload,
InvitePayload,
Expand Down
144 changes: 144 additions & 0 deletions src/util/userAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import os from 'os';

// Safe version extraction with fallback
let version = 'unknown';
try {
// Import version from package.json
// Note: In compiled code, this resolves correctly
version = require('../../package.json').version;
} catch {
// If package.json can't be loaded, use fallback version
// This ensures SDK initialization never fails
// Silently fail - User-Agent is telemetry, not critical functionality
}

/**
* Client information for detailed telemetry
*/
export interface ClientInfo {
/** SDK name */
sdk: string;
/** SDK version */
version: string;
/** Runtime (e.g., 'node') */
runtime: string;
/** Runtime version (e.g., '18.0.0') */
runtimeVersion: string;
/** Operating system platform */
platform: string;
/** Optional custom application identifier (e.g., 'MyApp/1.0.0') */
application?: string;
}

/**
* Safely get Node.js version, with fallback
* @returns Node.js version string without 'v' prefix
*/
function getNodeVersion(): string {
try {
if (process?.version) {
return process.version.replace('v', '');
}
} catch {
// Ignore error, use fallback
}
return 'unknown';
}

/**
* Safely get platform information, with fallback
* @returns Platform string (e.g., 'darwin', 'linux', 'win32')
*/
function getPlatform(): string {
try {
return os.platform();
} catch {
// If os.platform() fails, return fallback
return 'unknown';
}
}

/**
* Build standard User-Agent header value
* Format: envoy-integrations-sdk/2.4.4 node/18.0.0
* With custom app: envoy-integrations-sdk/2.4.4 node/18.0.0 MyApp/1.0.0
*
* This function is designed to never throw exceptions. If any error occurs,
* it returns a safe fallback value to ensure SDK initialization succeeds.
*
* @param customUserAgent Optional custom application identifier
* @returns User-Agent header value (never throws)
*/
export function buildUserAgent(customUserAgent?: string): string {
try {
const nodeVersion = getNodeVersion();
const baseUA = `envoy-integrations-sdk/${version} node/${nodeVersion}`;
return customUserAgent ? `${baseUA} ${customUserAgent}` : baseUA;
} catch {
// Critical fallback - should never happen, but ensures SDK always works
// Silently fail - User-Agent is telemetry, not critical functionality
return 'envoy-integrations-sdk/unknown node/unknown';
}
}

/**
* Build detailed client info for X-Envoy-Client-Info header
*
* This function is designed to never throw exceptions. If any error occurs
* during info collection, it uses safe fallback values.
*
* @param customUserAgent Optional custom application identifier
* @returns ClientInfo object (never throws)
*/
export function buildClientInfo(customUserAgent?: string): ClientInfo {
try {
const nodeVersion = getNodeVersion();
const platform = getPlatform();

const clientInfo: ClientInfo = {
sdk: 'envoy-integrations-sdk',
version,
runtime: 'node',
runtimeVersion: nodeVersion,
platform,
};

// Only add application if it's a non-empty string
if (customUserAgent) {
clientInfo.application = customUserAgent;
}

return clientInfo;
} catch {
// Critical fallback - return minimal safe info
// Silently fail - User-Agent is telemetry, not critical functionality
return {
sdk: 'envoy-integrations-sdk',
version: 'unknown',
runtime: 'node',
runtimeVersion: 'unknown',
platform: 'unknown',
};
}
}

/**
* Build X-Envoy-Client-Info header value (JSON string)
*
* This function is designed to never throw exceptions. If JSON serialization
* fails, it returns a minimal safe JSON string.
*
* @param customUserAgent Optional custom application identifier
* @returns JSON string for X-Envoy-Client-Info header (never throws)
*/
export function buildClientInfoHeader(customUserAgent?: string): string {
try {
const clientInfo = buildClientInfo(customUserAgent);
return JSON.stringify(clientInfo);
} catch {
// Critical fallback - return minimal valid JSON
// Silently fail - User-Agent is telemetry, not critical functionality
// Return minimal valid JSON that won't break parsing
return '{"sdk":"envoy-integrations-sdk","version":"unknown","runtime":"node","runtimeVersion":"unknown","platform":"unknown"}';
}
}
Loading
Loading