@findmore.eu/hs-utils
v1.8.0
Published
Utility functions for HubSpot and Azure Functions related to HubSpot API
Maintainers
Readme
@findmore.eu/hs-utils
Utility functions for HubSpot and Azure Functions related to HubSpot API.
Installation
npm install @findmore.eu/hs-utilsFeatures
- 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-blobpackage and connection string)
Blob Storage Setup:
Install the Azure Blob Storage SDK:
npm install @azure/storage-blobGet your Azure Storage connection string from Azure Portal:
- Go to your Storage Account → Access keys
- Copy the connection string
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=..." } }
- Azure Functions: Add to Application Settings as
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 dataappName(string): Application namelevel(string): Log level (INFO, WARN, ERROR, DEBUG, SUCCESS)message(string): Log messagedata(object, optional): Additional dataerror(object, optional): Error details- Any other custom fields
apiUrl(string): URL of the centralized logger API endpointlogSecret(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-v3andx-hubspot-request-timestampheaders
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 existsREQUIRED_FIELD_MISSING- A required field was not providedINVALID_FIELD_VALUE- A field has an invalid valueUNKNOWN_FIELD- A field doesn't exist in HubSpotOBJECT_NOT_FOUND- The requested record doesn't existRATE_LIMITED- Too many API requestsAUTH_FAILED- Authentication failedPERMISSION_DENIED- Insufficient permissionsBAD_REQUEST- Invalid request formatRESOURCE_LOCKED- Resource is currently lockedSERVER_ERROR- HubSpot server errorUNKNOWN- 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) setHubDbValuewrites 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_STRINGenvironment variable must be set- Requires
@azure/app-configurationpackage (included as dependency) - Returns
nullif 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 containingurl,method,body,headers, andhostnameoptions: Optional object withclientSecret(falls back toprocess.env.CLIENT_SECRET)
Returns: boolean | undefined
verifyCustomSignature(request, bodyText, logger?, requestId?, options?)
Verifies custom signature with optional Azure logging.
Parameters:
request: Object containingurl,method, andheadersbodyText: Request body as stringlogger: OptionalSimpleAzureLoggerinstancerequestId: Optional request ID for loggingoptions: Optional object withclientSecret
Returns: boolean
generateCustomSignature(url, method, timestamp, body?, options?)
Generates a signature for request validation.
Parameters:
url: Full request URLmethod: HTTP methodtimestamp: Request timestamp as stringbody: Optional request body objectoptions: Optional object withclientSecret(or useprocess.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 loglevel(string): Log level - one of:'INFO','WARN','ERROR','DEBUG','SUCCESS'message(string): The log messagedata(object, optional): Additional context dataerror(object, optional): Error details if applicable- Any other custom fields
apiUrl(string): Full URL of the centralized logger API endpointlogSecret(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-v3header - Timestamp (in milliseconds) sent in
x-hubspot-request-timestampheader - 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:falsedeleteOlderThanDays(number, optional): Delete log files older than X days. Only works ifenableFileLoggingistruestorageType('filesystem' | 'blob', optional): Storage type for file logging. Default:'filesystem'blobStorageConfig(BlobStorageConfig, optional): Required ifstorageTypeis'blob':connectionString(string): Azure Storage connection stringcontainerName(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 messagedata: Optional data object with additional contextrequestId: Optional request ID for tracking
success(message, duration?, data?, requestId?): void
Logs a success message with optional execution duration.
message: The log messageduration: Optional execution time in millisecondsdata: Optional data object with additional contextrequestId: Optional request ID for tracking
error(message, error?, data?, requestId?): void
Logs an error message.
message: The log messageerror: Optional error object or valuedata: Optional data object with additional contextrequestId: Optional request ID for tracking
warn(message, data?, requestId?): void
Logs a warning message.
message: The log messagedata: Optional data object with additional contextrequestId: Optional request ID for tracking
debug(message, data?, requestId?): void
Logs a debug message.
message: The log messagedata: Optional data object with additional contextrequestId: Optional request ID for tracking
log(entry): void
Main log method with full flexibility. Accepts any log entry with custom fields.
entry: Object withlevel,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 measuredfn: Async function to execute and measurerequestId: 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
nullif 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 enabledstorageType:'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 existscanWrite: 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 prefixfileLogging: Whether file logging is enabledstorageType:'filesystem'or'blob'bufferSize: Current buffer sizemaxBufferSize: Maximum buffer size (100)fileInfo: Result fromgetLogFileInfo()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 itemvalue: Any serializable value to cachettlMs: 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
nullif 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:
- In-Memory Priority: Checks in-memory cache first for fast access
- File Fallback: If not in memory, loads from file (persists across invocations)
- Auto-Load: Automatically loads valid entries from file on instantiation
- Auto-Save: Automatically saves to file when using
set() - Auto-Expire: Expired items are automatically filtered out
- 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
categoryandmessage - Axios error response (will be parsed via
parseHubSpotErrorfirst) - Any object that can be normalized
- Direct HubSpot error response with
Returns: NormalizedHubSpotError object with:
code:HubSpotSimpleErrorCodeenum valueuserMessage: User-friendly error messagedetails: Optional object containing:category: Original HubSpot error categorycorrelationId: HubSpot correlation ID for debuggingrawMessage: Original error messageproperty: 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-isHubSpotSimpleErrorCode
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'sAPP_KEY, withoutbase64: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 encryptkey: 32-byte key (fromgetAppKey)
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 payloadkey: 32-byte key (fromgetAppKey)
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 namename: Key/name to look upoptions: OptionalHubDbKeyValueOptions:nameColumn(default:'name'): Column for the keyvalueColumn(default:'value'): Column for the valueuseDraft(default:false): Read from draft tableapiToken: HubSpot API token (falls back toprocess.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 namename: Key/name to setvalue: Value to storeoptions: OptionalHubDbKeyValueOptions(same asgetHubDbValue)
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 keyvalue: 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
- Secret Key Required: Only parties with the secret can generate valid signatures
- One-Way Function: Cannot be reversed to get the original data
- Collision Resistant: Extremely difficult to find two inputs with the same hash
- Industry Standard: Used by HubSpot, AWS, GitHub, and other major platforms
License
CC-BY-NC-ND-4.0
Author
findmore.eu [email protected]
