npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@findmore.eu/hs-utils

v1.8.0

Published

Utility functions for HubSpot and Azure Functions related to HubSpot API

Readme

@findmore.eu/hs-utils

Utility functions for HubSpot and Azure Functions related to HubSpot API.

Installation

npm install @findmore.eu/hs-utils

Features

  • Signature Verification: Verify HubSpot webhook signatures (both standard HMAC-SHA256 and custom implementations)
  • Signature Generation: Generate custom signatures for request validation with HMAC-SHA256 or Base64 fallback
  • Azure Logging: Simple Azure-friendly logger for Azure Functions with file logging (filesystem or blob storage) and auto-cleanup
  • Centralized Logger: Send logs to a centralized logging service with HMAC-SHA256 signature verification
  • Simple Cache: In-memory and file-based cache with TTL support for Azure Functions
  • Error Normalization: Convert raw HubSpot API errors into simple, user-friendly messages with stable error codes
  • Secure Text: Laravel-compatible AES-256-CBC encryption/decryption with HMAC-SHA256 integrity verification
  • HubDB Key-Value: Get and set key-value pairs in HubSpot HubDB tables
  • Azure App Configuration: Read and write configuration values from Azure App Configuration service
  • TypeScript Support: Full TypeScript type definitions included

Usage

Verify HubSpot Signature

import { verifyHubspotSignature } from '@findmore.eu/hs-utils';

const request = {
  url: '/webhook',
  method: 'POST',
  body: { data: 'example' },
  headers: {
    'x-hubspot-signature-v3': 'signature_here',
    'x-hubspot-request-timestamp': '1234567890'
  },
  hostname: 'your-domain.com'
};

const isValid = verifyHubspotSignature(request, {
  clientSecret: 'your-client-secret' // or use process.env.CLIENT_SECRET
});

Verify Custom Signature

import { verifyCustomSignature } from '@findmore.eu/hs-utils';

const request = {
  url: 'https://your-domain.com/webhook',
  method: 'POST',
  headers: {
    get: (name: string) => {
      // Return header value
    }
  }
};

const bodyText = JSON.stringify({ data: 'example' });

const isValid = verifyCustomSignature(request, bodyText, undefined, undefined, {
  clientSecret: 'your-client-secret'
});

Generate Custom Signature

import { generateCustomSignature } from '@findmore.eu/hs-utils';

// Secure: Using HMAC-SHA256 with secret (recommended)
const secureSignature = generateCustomSignature(
  'https://your-domain.com/webhook',
  'POST',
  '1234567890',
  { data: 'example' },
  { clientSecret: 'your-secret-key' }
);

// Fallback: Base64 encoding without secret (NOT secure, for backward compatibility only)
const insecureSignature = generateCustomSignature(
  'https://your-domain.com/webhook',
  'POST',
  '1234567890',
  { data: 'example' }
);

Azure Logger

import { SimpleAzureLogger } from '@findmore.eu/hs-utils';

// Create logger instance (console only)
const logger = new SimpleAzureLogger('my-function-name');

// File system logging with auto-cleanup (delete files older than 7 days)
const loggerWithCleanup = new SimpleAzureLogger('my-function-name', true, 7);

// Blob storage logging (requires @azure/storage-blob package)
const blobLogger = new SimpleAzureLogger(
    'my-function-name',
    true,                    // enableFileLogging
    7,                       // deleteOlderThanDays
    'blob',                  // storageType: 'filesystem' (default) or 'blob'
    {
        connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
        containerName: 'logs'  // Optional, defaults to 'logs'
    }
);

// Generate a request ID for tracking
const requestId = logger.generateRequestId();

// Log levels with appropriate use cases:

// INFO - Normal operation milestone
logger.info('Processing request', { userId: '123' }, requestId);

// SUCCESS - Operation completed successfully (with optional duration in ms)
logger.success('Request completed', 1234, { result: 'success' }, requestId);

// ERROR - Something failed (with error object)
logger.error('Request failed', new Error('Something went wrong'), { context: 'details' }, requestId);

// WARN - Potential issue, but continuing
logger.warn('API rate limit approaching', { remaining: 10 }, requestId);

// DEBUG - Detailed debugging information
logger.debug('Cache lookup', { key: 'user:123', hit: true }, requestId);

// Advanced: Custom log with any fields
logger.log({
    level: 'INFO',
    message: 'Custom log entry',
    customField1: 'value1',
    customField2: 42,
    requestId
});

// Measure execution time of async operations
const result = await logger.measureTime(
    'fetchHubSpotData',
    async () => {
        return await fetchData();
    },
    requestId
);

// Reading logs (async - works with both filesystem and blob storage)
const todayLogs = await logger.tryReadLogs();
const specificDateLogs = await logger.tryReadLogs('2024-01-15');

// List available log files (async)
const availableFiles = await logger.tryListLogFiles();

// Get logger status (async)
const status = await logger.getLoggerStatus();
// Returns: { logPrefix, fileLogging, storageType, bufferSize, maxBufferSize, fileInfo, availableLogFiles }

Storage Types:

  • 'filesystem' (default): Logs to Azure's file system (D:\home\LogFiles\Application\Functions\{logPrefix}\ on Windows, /home/LogFiles/Application/Functions/{logPrefix}/ on Linux)
  • 'blob': Logs to Azure Blob Storage (requires @azure/storage-blob package and connection string)

Blob Storage Setup:

  1. Install the Azure Blob Storage SDK:

    npm install @azure/storage-blob
  2. Get your Azure Storage connection string from Azure Portal:

    • Go to your Storage Account → Access keys
    • Copy the connection string
  3. Set it as an environment variable:

    • Azure Functions: Add to Application Settings as AZURE_STORAGE_CONNECTION_STRING
    • Local Development: Add to local.settings.json:
      {
        "Values": {
          "AZURE_STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=https;AccountName=..."
        }
      }

Benefits of Blob Storage:

  • ✅ Persistent storage (survives app restarts and deployments)
  • ✅ Accessible via Azure Portal, Storage Explorer, or REST API
  • ✅ Scalable (no disk space limits)
  • ✅ Long-term retention
  • ✅ Easy access without Kudu/SSH

Send Log to Centralized Logger

Send logs to a centralized logging service with HMAC-SHA256 signature verification.

import { SendLogToCentralizedLogger } from '@findmore.eu/hs-utils';

const logEntry = {
  appName: 'my-app',
  level: 'INFO',
  message: 'Processing request',
  data: { userId: '123', action: 'login' },
  requestId: 'req-123'
};

try {
  const response = await SendLogToCentralizedLogger(
    logEntry,
    'https://your-centralized-logger.com/api/logs',
    'your-log-secret-key'
  );
  console.log('Log sent successfully:', response);
} catch (error) {
  console.error('Failed to send log:', error);
}

Parameters:

  • logEntry: Object containing log data
    • appName (string): Application name
    • level (string): Log level (INFO, WARN, ERROR, DEBUG, SUCCESS)
    • message (string): Log message
    • data (object, optional): Additional data
    • error (object, optional): Error details
    • Any other custom fields
  • apiUrl (string): URL of the centralized logger API endpoint
  • logSecret (string): Secret key for HMAC-SHA256 signature generation

Returns: Promise<Object> - Response from the centralized logger API

Security:

  • Automatically generates HMAC-SHA256 signature for request authentication
  • Includes timestamp to prevent replay attacks
  • Uses x-hubspot-signature-v3 and x-hubspot-request-timestamp headers

Simple Cache

A persistent cache system that works across Azure Function invocations with both in-memory and file-based storage.

import { SimpleCache } from '@findmore.eu/hs-utils';

// Create cache instance
// Default: 24 hours TTL, 'function_cache.json', 'windows'
const cache = new SimpleCache();

// Custom configuration
const customCache = new SimpleCache(
    12 * 60 * 60 * 1000,         // 12 hours TTL (in milliseconds)
    'my_custom_cache.json',      // Custom filename
    'linux'                       // OS: 'linux' or 'windows'
);

// Store data with default TTL
cache.set('user:123', { name: 'John Doe', email: '[email protected]' });

// Store data with custom TTL (1 hour)
cache.set('temporary:data', { value: 'temp' }, 60 * 60 * 1000);

// Retrieve data (returns null if expired or not found)
const user = cache.get<{ name: string; email: string }>('user:123');
if (user) {
    console.log(user.name); // 'John Doe'
}

// Get all cache entries
const allEntries = cache.getAll();

// Get cache size
const size = cache.length();

// Practical example: Cache HubSpot schema
const schemaKey = `schema:${objectType}`;
let schema = cache.get(schemaKey);

if (!schema) {
    // Not in cache, fetch from API
    schema = await fetchHubSpotSchema(objectType);
    
    // Store in cache for 24 hours
    cache.set(schemaKey, schema);
}

return schema;

Key Features:

  • Dual Storage: In-memory (fast) + File-based (persistent across invocations)
  • TTL Support: Automatic expiration of cached items
  • Azure-Optimized: Handles temporary directories for both Linux and Windows
  • Built-in Logging: Uses SimpleAzureLogger with 7-day auto-cleanup
  • Type-Safe: Full TypeScript support with generics

Cache Locations:

  • Windows: D:\local\Temp\function_cache.json
  • Linux: /tmp/function_cache.json

HubSpot Error Normalization

Convert raw HubSpot API errors into simple, user-friendly messages with stable error codes. Designed to be resilient to HubSpot message wording changes by using category + pattern matching instead of exact string matching.

import { 
  normalizeHubSpotError, 
  parseHubSpotError,
  HubSpotSimpleErrorCode 
} from '@findmore.eu/hs-utils';

// Example 1: Direct normalization from HubSpot error response
try {
  await hubspotApi.createContact({ email: '[email protected]' });
} catch (err) {
  const normalized = normalizeHubSpotError(err);
  
  console.log(normalized.code);        // 'DUPLICATE_UNIQUE_VALUE'
  console.log(normalized.userMessage); // 'A record with email "[email protected]" already exists.'
  
  // Handle specific error types
  if (normalized.code === HubSpotSimpleErrorCode.DUPLICATE_UNIQUE_VALUE) {
    // Show user-friendly message
    showError(normalized.userMessage);
  }
}

// Example 2: Parse error from Axios response
try {
  await axios.post('https://api.hubapi.com/...', data);
} catch (err) {
  // parseHubSpotError handles Axios-style errors (err.response.data)
  const hubspotError = parseHubSpotError(err);
  
  if (hubspotError) {
    const normalized = normalizeHubSpotError(hubspotError);
    console.log(normalized.userMessage);
  }
}

// Example 3: Handle multiple error types
try {
  await hubspotApi.updateContact(contactId, data);
} catch (err) {
  const normalized = normalizeHubSpotError(err);
  
  switch (normalized.code) {
    case HubSpotSimpleErrorCode.DUPLICATE_UNIQUE_VALUE:
      showError('This value already exists. Please use a different one.');
      break;
    case HubSpotSimpleErrorCode.REQUIRED_FIELD_MISSING:
      showError(`Please fill in: ${normalized.details?.property}`);
      break;
    case HubSpotSimpleErrorCode.RATE_LIMITED:
      showError('Too many requests. Please wait a moment.');
      break;
    case HubSpotSimpleErrorCode.AUTH_FAILED:
      // Trigger re-authentication flow
      redirectToAuth();
      break;
    default:
      showError(normalized.userMessage);
  }
}

Key Features:

  • User-Friendly Messages: Converts technical HubSpot errors into clear, actionable messages
  • Stable Error Codes: Use consistent error codes regardless of HubSpot API changes
  • Pattern Matching: Resilient to HubSpot message wording changes
  • Multiple Error Sources: Handles direct errors, Axios responses, and wrapped serverless errors
  • Type-Safe: Full TypeScript support with enums and interfaces
  • Detailed Context: Includes correlation IDs, property names, and raw messages for debugging

Supported Error Types:

  • DUPLICATE_UNIQUE_VALUE - A unique field value already exists
  • REQUIRED_FIELD_MISSING - A required field was not provided
  • INVALID_FIELD_VALUE - A field has an invalid value
  • UNKNOWN_FIELD - A field doesn't exist in HubSpot
  • OBJECT_NOT_FOUND - The requested record doesn't exist
  • RATE_LIMITED - Too many API requests
  • AUTH_FAILED - Authentication failed
  • PERMISSION_DENIED - Insufficient permissions
  • BAD_REQUEST - Invalid request format
  • RESOURCE_LOCKED - Resource is currently locked
  • SERVER_ERROR - HubSpot server error
  • UNKNOWN - Unrecognized error type

Secure Text (Laravel-Compatible Encryption)

Encrypt and decrypt strings using Laravel-compatible AES-256-CBC with HMAC-SHA256 integrity verification. Interoperable with Laravel's Crypt::encryptString() and Crypt::decryptString().

import { getAppKey, encryptString, decryptString } from '@findmore.eu/hs-utils';

// Get key from Laravel-style base64 APP_KEY (remove "base64:" prefix if present)
const key = getAppKey('your-32-byte-base64-encoded-key=');

// Encrypt
const encrypted = encryptString('sensitive data', key);

// Decrypt
const decrypted = decryptString(encrypted, key);

Key requirements:

  • Key must be 32 bytes (decoded from base64)
  • Use the same key format as Laravel's APP_KEY (base64-encoded, 44 characters)

HubDB Key-Value

Get and set key-value pairs in HubSpot HubDB tables. Useful for storing configuration, feature flags, or simple data without a database.

import { getHubDbValue, setHubDbValue } from '@findmore.eu/hs-utils';

// Get a value (uses HUBSPOT_API_TOKEN from env by default)
const endpoint = await getHubDbValue('my_config_table', 'api_endpoint');
if (endpoint) {
  console.log('API endpoint:', endpoint);
}

// Set a value (writes to draft; publish in HubSpot to go live)
await setHubDbValue('my_config_table', 'api_endpoint', 'https://api.example.com');

// Custom column names
const value = await getHubDbValue('my_table', 'config_key', {
  nameColumn: 'key',
  valueColumn: 'config_value',
});

// Use draft table for reads
const draftValue = await getHubDbValue('my_table', 'key', { useDraft: true });

Table requirements:

  • Table must have columns for the key (default: name) and value (default: value)
  • setHubDbValue writes to the draft table; publish in HubSpot to make changes live

Azure App Configuration

Read and write configuration values from Azure App Configuration service. Requires AZURE_APP_CONFIG_CONNECTION_STRING in your environment.

import { getConfigValue, setConfigValue } from '@findmore.eu/hs-utils';

// Get a config value
const apiUrl = await getConfigValue('MyApp:ApiUrl');
if (apiUrl) {
  console.log('API URL:', apiUrl);
}

// Set a config value
await setConfigValue('MyApp:FeatureFlag', 'true');

Requirements:

  • AZURE_APP_CONFIG_CONNECTION_STRING environment variable must be set
  • Requires @azure/app-configuration package (included as dependency)
  • Returns null if the connection string is missing or the key is not found

API Reference

verifyHubspotSignature(request, options?)

Verifies HubSpot webhook signature using HMAC-SHA256.

Parameters:

  • request: Object containing url, method, body, headers, and hostname
  • options: Optional object with clientSecret (falls back to process.env.CLIENT_SECRET)

Returns: boolean | undefined

verifyCustomSignature(request, bodyText, logger?, requestId?, options?)

Verifies custom signature with optional Azure logging.

Parameters:

  • request: Object containing url, method, and headers
  • bodyText: Request body as string
  • logger: Optional SimpleAzureLogger instance
  • requestId: Optional request ID for logging
  • options: Optional object with clientSecret

Returns: boolean

generateCustomSignature(url, method, timestamp, body?, options?)

Generates a signature for request validation.

Parameters:

  • url: Full request URL
  • method: HTTP method
  • timestamp: Request timestamp as string
  • body: Optional request body object
  • options: Optional object with clientSecret (or use process.env.CLIENT_SECRET)

Returns: string

Behavior:

  • With clientSecret: Uses HMAC-SHA256 (cryptographically secure)
  • Without clientSecret: Falls back to Base64 encoding (NOT secure, for backward compatibility only)

SendLogToCentralizedLogger(logEntry, apiUrl, logSecret)

Sends a log entry to a centralized logging service with HMAC-SHA256 signature authentication.

Parameters:

  • logEntry: Object containing log data with the following structure:
    • appName (string): Name of the application sending the log
    • level (string): Log level - one of: 'INFO', 'WARN', 'ERROR', 'DEBUG', 'SUCCESS'
    • message (string): The log message
    • data (object, optional): Additional context data
    • error (object, optional): Error details if applicable
    • Any other custom fields
  • apiUrl (string): Full URL of the centralized logger API endpoint
  • logSecret (string): Secret key used for HMAC-SHA256 signature generation

Returns: Promise<Object> - Response from the centralized logger API

Throws: Error if the request fails or the API returns an error

Implementation Details:

  • Generates signature using: HMAC-SHA256(METHOD + URL + BODY + TIMESTAMP, logSecret)
  • Signature sent in x-hubspot-signature-v3 header
  • Timestamp (in milliseconds) sent in x-hubspot-request-timestamp header
  • Request body is JSON-encoded log entry

SimpleAzureLogger(logPrefix?, enableFileLogging?, deleteOlderThanDays?, storageType?, blobStorageConfig?)

Creates a new logger instance for Azure Functions.

Constructor Parameters:

  • logPrefix (string, optional): Name/prefix for logs (e.g., function name). Default: 'azure-function'
  • enableFileLogging (boolean, optional): Whether to attempt file logging. Default: false
  • deleteOlderThanDays (number, optional): Delete log files older than X days. Only works if enableFileLogging is true
  • storageType ('filesystem' | 'blob', optional): Storage type for file logging. Default: 'filesystem'
  • blobStorageConfig (BlobStorageConfig, optional): Required if storageType is 'blob':
    • connectionString (string): Azure Storage connection string
    • containerName (string, optional): Container name for logs. Default: 'logs'

Methods:

generateRequestId(): string

Generates a unique request ID for tracking related log entries.

info(message, data?, requestId?): void

Logs an informational message.

  • message: The log message
  • data: Optional data object with additional context
  • requestId: Optional request ID for tracking

success(message, duration?, data?, requestId?): void

Logs a success message with optional execution duration.

  • message: The log message
  • duration: Optional execution time in milliseconds
  • data: Optional data object with additional context
  • requestId: Optional request ID for tracking

error(message, error?, data?, requestId?): void

Logs an error message.

  • message: The log message
  • error: Optional error object or value
  • data: Optional data object with additional context
  • requestId: Optional request ID for tracking

warn(message, data?, requestId?): void

Logs a warning message.

  • message: The log message
  • data: Optional data object with additional context
  • requestId: Optional request ID for tracking

debug(message, data?, requestId?): void

Logs a debug message.

  • message: The log message
  • data: Optional data object with additional context
  • requestId: Optional request ID for tracking

log(entry): void

Main log method with full flexibility. Accepts any log entry with custom fields.

  • entry: Object with level, message, and any additional custom fields

measureTime<T>(operation, fn, requestId?): Promise<T>

Measures execution time of an async function and logs the result.

  • operation: Name of the operation being measured
  • fn: Async function to execute and measure
  • requestId: Optional request ID for tracking
  • Returns: The result of the async function

getBufferedLogs(): string[]

Returns the last 100 log entries from memory.

clearBuffer(): void

Clears the in-memory log buffer.

exportBufferAsJson(): string

Exports buffered logs as JSON string.

tryReadLogs(date?): Promise<string | null>

Attempts to read logs from file or blob storage (if file logging is enabled).

  • date: Optional date in YYYY-MM-DD format. Default: today
  • Returns: Promise resolving to log content string or null if not found
  • Note: This method is async and works with both filesystem and blob storage

tryListLogFiles(): Promise<string[]>

Lists available log files (most recent first).

  • Returns: Promise resolving to array of log file names
  • Note: This method is async and works with both filesystem and blob storage

getLogFileInfo(): Promise<object>

Returns information about log file location and status.

  • Returns: Promise resolving to object with:
    • enabled: Whether file logging is enabled
    • storageType: 'filesystem' or 'blob'
    • folder: File system folder path (null for blob storage)
    • filePath: File system file path (null for blob storage)
    • blobName: Blob name (null for filesystem)
    • containerName: Blob container name (null for filesystem)
    • exists: Whether the log file/blob exists
    • canWrite: Whether writing is possible
  • Note: This method is async

getLoggerStatus(): Promise<object>

Returns a summary of the logger status including buffer size and available log files.

  • Returns: Promise resolving to object with:
    • logPrefix: Logger prefix
    • fileLogging: Whether file logging is enabled
    • storageType: 'filesystem' or 'blob'
    • bufferSize: Current buffer size
    • maxBufferSize: Maximum buffer size (100)
    • fileInfo: Result from getLogFileInfo()
    • availableLogFiles: Array of available log files
  • Note: This method is async

StorageType

Type for storage options:

type StorageType = 'filesystem' | 'blob';
  • 'filesystem': Logs to Azure's file system (default)
  • 'blob': Logs to Azure Blob Storage

BlobStorageConfig

Interface for blob storage configuration:

interface BlobStorageConfig {
  connectionString: string;  // Azure Storage connection string (required)
  containerName?: string;     // Container name for logs (optional, defaults to 'logs')
}

Example:

import { SimpleAzureLogger, BlobStorageConfig } from '@findmore.eu/hs-utils';

const config: BlobStorageConfig = {
  connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!,
  containerName: 'my-logs'
};

const logger = new SimpleAzureLogger('my-function', true, 7, 'blob', config);

SimpleCache(cacheTtl?, filename?, osSystem?)

Creates a persistent cache instance for Azure Functions.

Constructor Parameters:

  • cacheTtl (number, optional): Default TTL for cached items in milliseconds. Default: 24 * 60 * 60 * 1000 (24 hours)
  • filename (string, optional): Cache file name. Default: 'function_cache.json'
  • osSystem ('linux' | 'windows', optional): Operating system for temp directory. Default: 'windows'

Methods:

set(key, value, ttlMs?): void

Stores a value in cache with optional TTL.

  • key: String key to identify the cached item
  • value: Any serializable value to cache
  • ttlMs: Optional TTL in milliseconds (overrides default TTL)

get<T>(key): T | null

Retrieves a value from cache.

  • key: String key of the cached item
  • Returns: The cached value or null if not found or expired
  • Supports TypeScript generics for type safety

getAll(): Map<string, { data: any, expiry: number }>

Returns all cache entries with their data and expiry times.

length(): number

Returns the number of items currently in cache.

How it works:

  1. In-Memory Priority: Checks in-memory cache first for fast access
  2. File Fallback: If not in memory, loads from file (persists across invocations)
  3. Auto-Load: Automatically loads valid entries from file on instantiation
  4. Auto-Save: Automatically saves to file when using set()
  5. Auto-Expire: Expired items are automatically filtered out
  6. Logging: Built-in logging with SimpleAzureLogger (7-day auto-cleanup)

normalizeHubSpotError(err)

Converts raw HubSpot API errors into normalized, user-friendly error messages.

Parameters:

  • err: HubSpot error response object or any error object. Can be:
    • Direct HubSpot error response with category and message
    • Axios error response (will be parsed via parseHubSpotError first)
    • Any object that can be normalized

Returns: NormalizedHubSpotError object with:

  • code: HubSpotSimpleErrorCode enum value
  • userMessage: User-friendly error message
  • details: Optional object containing:
    • category: Original HubSpot error category
    • correlationId: HubSpot correlation ID for debugging
    • rawMessage: Original error message
    • property: Field/property name (if applicable)
    • existingObjectId: ID of existing object (for duplicates)
    • targetObjectId: ID of target object (for duplicates)

Example:

const normalized = normalizeHubSpotError({
  category: 'VALIDATION_ERROR',
  message: 'A contact with email "[email protected]" already exists',
  correlationId: 'abc123'
});

// Result:
// {
//   code: 'DUPLICATE_UNIQUE_VALUE',
//   userMessage: 'A record with email "[email protected]" already exists.',
//   details: {
//     category: 'VALIDATION_ERROR',
//     correlationId: 'abc123',
//     rawMessage: 'A contact with email "[email protected]" already exists',
//     property: 'email'
//   }
// }

parseHubSpotError(err)

Parses errors from various sources (direct errors, Axios responses, wrapped serverless errors) into a standardized HubSpot error format.

Parameters:

  • err: Error object from various sources:
    • Direct HubSpot error response
    • Axios error (checks err.response.data)
    • Wrapped serverless error (string with JSON body)

Returns: HubSpotErrorResponse | null - Parsed error object or null if parsing fails

Example:

// Axios error
try {
  await axios.post('https://api.hubapi.com/...', data);
} catch (err) {
  const hubspotError = parseHubSpotError(err);
  // Returns err.response.data if it's a HubSpot error, null otherwise
}

// Direct error
const error = {
  category: 'OBJECT_NOT_FOUND',
  message: 'Contact not found'
};
const parsed = parseHubSpotError(error); // Returns the error as-is

HubSpotSimpleErrorCode

Enum of stable error codes that your application can rely on:

enum HubSpotSimpleErrorCode {
  DUPLICATE_UNIQUE_VALUE = 'DUPLICATE_UNIQUE_VALUE',
  REQUIRED_FIELD_MISSING = 'REQUIRED_FIELD_MISSING',
  INVALID_FIELD_VALUE = 'INVALID_FIELD_VALUE',
  UNKNOWN_FIELD = 'UNKNOWN_FIELD',
  OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
  RATE_LIMITED = 'RATE_LIMITED',
  AUTH_FAILED = 'AUTH_FAILED',
  PERMISSION_DENIED = 'PERMISSION_DENIED',
  BAD_REQUEST = 'BAD_REQUEST',
  SERVER_ERROR = 'SERVER_ERROR',
  RESOURCE_LOCKED = 'RESOURCE_LOCKED',
  UNKNOWN = 'UNKNOWN'
}

NormalizedHubSpotError

Interface for normalized error objects:

interface NormalizedHubSpotError {
  code: HubSpotSimpleErrorCode;
  userMessage: string;
  details?: {
    category?: string;
    correlationId?: string;
    rawMessage?: string;
    property?: string;
    existingObjectId?: string;
    targetObjectId?: string;
  };
}

HubSpotErrorResponse

Interface for raw HubSpot error responses:

interface HubSpotErrorResponse {
  status?: 'error';
  message?: string;
  category?: string;
  correlationId?: string;
  errors?: HubSpotErrorDetail[];
}

Secure Text

getAppKey(APP_KEY_B64: string): Buffer

Decodes a Laravel-style base64 APP_KEY and validates it is 32 bytes for AES-256.

Parameters:

  • APP_KEY_B64: Base64-encoded 32-byte key (e.g. from Laravel's APP_KEY, without base64: prefix)

Returns: Buffer - 32-byte key

Throws: Error if key is not 32 bytes after decoding

encryptString(plaintext: string, key: Buffer): string

Encrypts a string using Laravel-compatible AES-256-CBC with HMAC-SHA256 integrity.

Parameters:

  • plaintext: String to encrypt
  • key: 32-byte key (from getAppKey)

Returns: string - Base64-encoded payload (iv + ciphertext + mac)

decryptString(encryptedB64: string, key: Buffer): string

Decrypts a string encrypted with encryptString or Laravel's Crypt::encryptString().

Parameters:

  • encryptedB64: Base64-encoded encrypted payload
  • key: 32-byte key (from getAppKey)

Returns: string - Decrypted plaintext

Throws: Error if MAC verification fails (tampered or wrong key)

HubDB Key-Value

getHubDbValue(tableIdOrName, name, options?): Promise<string | null | undefined>

Gets a value from a HubDB table by key.

Parameters:

  • tableIdOrName: HubDB table ID or name
  • name: Key/name to look up
  • options: Optional HubDbKeyValueOptions:
    • nameColumn (default: 'name'): Column for the key
    • valueColumn (default: 'value'): Column for the value
    • useDraft (default: false): Read from draft table
    • apiToken: HubSpot API token (falls back to process.env.HUBSPOT_API_TOKEN)

Returns: Promise<string | null | undefined> - Value or null if not found

setHubDbValue(tableIdOrName, name, value, options?): Promise<void>

Sets a value in a HubDB table. Creates a new row if not found, updates existing. Writes to draft table.

Parameters:

  • tableIdOrName: HubDB table ID or name
  • name: Key/name to set
  • value: Value to store
  • options: Optional HubDbKeyValueOptions (same as getHubDbValue)

Azure App Configuration

getConfigValue(key: string): Promise<string | null | undefined>

Gets a configuration value from Azure App Configuration.

Parameters:

  • key: Configuration key (e.g. MyApp:ApiUrl)

Returns: Promise<string | null | undefined> - Value or null if not found or connection string missing

setConfigValue(key: string, value: string): Promise<void>

Sets a configuration value in Azure App Configuration.

Parameters:

  • key: Configuration key
  • value: Value to store

Note: Both functions require AZURE_APP_CONFIG_CONNECTION_STRING in the environment. They return/exit silently if it is not set.

Security Notes

Recommended: Use HMAC-SHA256 with Secret

Always provide a clientSecret to use secure HMAC-SHA256 signing:

const signature = generateCustomSignature(url, method, timestamp, body, {
  clientSecret: 'your-secret-key'
});

Fallback Mode (Insecure)

Without a clientSecret, the function falls back to Base64 encoding:

  • Not cryptographically secure
  • Anyone can decode and recreate signatures
  • Only use for backward compatibility with existing systems
  • Should be migrated to HMAC-SHA256 as soon as possible

Why HMAC-SHA256 is Secure

  1. Secret Key Required: Only parties with the secret can generate valid signatures
  2. One-Way Function: Cannot be reversed to get the original data
  3. Collision Resistant: Extremely difficult to find two inputs with the same hash
  4. Industry Standard: Used by HubSpot, AWS, GitHub, and other major platforms

License

CC-BY-NC-ND-4.0

Author

findmore.eu [email protected]