0
+ INFO
+ 0
+ INFO
+ (\d+)/i);
+ const messageMatch = response.match(/([^<]+)/i);
+
+ if (codeMatch) {
+ error.ofxCode = codeMatch[1];
+ }
+ if (messageMatch) {
+ error.ofxMessage = messageMatch[1].trim();
+ }
+ } catch (parseError) {
+ debug('Failed to parse OFX error information:', parseError.message);
+ }
+};
+
+/**
+ * Update response time metrics
+ * @param {number} responseTime - Response time in milliseconds
+ */
+ConnectionPool.prototype.updateResponseTimeMetrics = function (responseTime) {
+ this.metrics.requestTimes.push(responseTime);
+
+ // Keep only last 100 response times for average calculation
+ if (this.metrics.requestTimes.length > 100) {
+ this.metrics.requestTimes.shift();
+ }
+
+ // Calculate average response time
+ const sum = this.metrics.requestTimes.reduce((a, b) => {
+ return a + b;
+ }, 0);
+ this.metrics.averageResponseTime = Math.round(sum / this.metrics.requestTimes.length);
+};
+
+/**
+ * Get current pool metrics including retry manager metrics
+ * @returns {object} Current metrics
+ */
+ConnectionPool.prototype.getMetrics = function () {
+ const poolStats = {};
+
+ // Get connection statistics from agents
+ this.agents.forEach((agent, key) => {
+ const sockets = agent.sockets || {};
+ const freeSockets = agent.freeSockets || {};
+ const requests = agent.requests || {};
+
+ poolStats[key] = {
+ sockets: Object.keys(sockets).reduce((count, host) => {
+ return count + sockets[host].length;
+ }, 0),
+ freeSockets: Object.keys(freeSockets).reduce((count, host) => {
+ return count + freeSockets[host].length;
+ }, 0),
+ requests: Object.keys(requests).reduce((count, host) => {
+ return count + requests[host].length;
+ }, 0)
+ };
+ });
+
+ const result = Object.assign({}, this.metrics, {
+ poolStats: poolStats,
+ agentCount: this.agents.size
+ });
+
+ // Include retry manager metrics if available
+ if (this.retryManager) {
+ result.retryMetrics = this.retryManager.getMetrics();
+ }
+
+ return result;
+};
+
+/**
+ * Report current metrics (called periodically if enabled)
+ */
+ConnectionPool.prototype.reportMetrics = function () {
+ const metrics = this.getMetrics();
+ debug('Pool metrics:', JSON.stringify(metrics, null, 2));
+};
+
+/**
+ * Close all connections and clean up resources
+ */
+ConnectionPool.prototype.destroy = function () {
+ debug('Destroying connection pool');
+
+ // Clear metrics timer
+ if (this.metricsTimer) {
+ clearInterval(this.metricsTimer);
+ this.metricsTimer = null;
+ }
+
+ // Destroy all agents
+ this.agents.forEach(agent => {
+ if (agent.destroy) {
+ agent.destroy();
+ }
+ });
+
+ this.agents.clear();
+ this.metrics = null;
+
+ // Clean up retry manager
+ if (this.retryManager) {
+ this.retryManager.resetMetrics();
+ this.retryManager = null;
+ }
+};
+
+// Export the ConnectionPool class
+module.exports = ConnectionPool;
diff --git a/lib/errors.js b/lib/errors.js
new file mode 100644
index 0000000..a1a30be
--- /dev/null
+++ b/lib/errors.js
@@ -0,0 +1,597 @@
+/*!
+ * Structured Error Classes for banking.js
+ * Provides comprehensive error handling and classification for banking operations
+ */
+
+const debug = require('debug')('banking:errors');
+
+/**
+ * Generate a simple UUID for correlation IDs
+ * @param {number} len - Length of the UUID
+ * @returns {string} - Generated UUID
+ */
+function generateCorrelationId(len = 16) {
+ const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
+ const chars = CHARS;
+ const uuid = [];
+
+ for (let i = 0; i < len; i++) {
+ uuid[i] = chars[Math.floor(Math.random() * chars.length)];
+ }
+
+ return uuid.join('');
+}
+
+/**
+ * Base error class for all banking operations
+ * Provides structured error information, correlation IDs, and PCI-compliant logging
+ */
+class BankingError extends Error {
+ constructor(message, options = {}) {
+ super(message);
+
+ // Set error name and maintain prototype chain
+ this.name = this.constructor.name;
+ Error.captureStackTrace(this, this.constructor);
+
+ // Core error properties
+ this.code = options.code || 'BANKING_ERROR';
+ this.correlationId = options.correlationId || generateCorrelationId(16);
+ this.timestamp = options.timestamp || new Date().toISOString();
+ this.category = options.category || 'UNKNOWN';
+
+ // Classification and retry information
+ this.retryable = options.retryable !== undefined ? options.retryable : false;
+ this.retryAfter = options.retryAfter || null;
+ this.maxRetries = options.maxRetries || 0;
+
+ // Context information (PCI-compliant - no sensitive data)
+ this.bankingContext = {
+ fid: options.fid || null,
+ fidOrg: options.fidOrg || null,
+ operationType: options.operationType || null,
+ accountType: options.accountType || null,
+ // Never include account numbers, credentials, or transaction details
+ url: options.url ? this._sanitizeUrl(options.url) : null
+ };
+
+ // Technical details for debugging
+ this.technicalDetails = {
+ originalError: options.originalError || null,
+ httpStatus: options.httpStatus || null,
+ ofxStatus: options.ofxStatus || null,
+ requestId: options.requestId || null,
+ userAgent: options.userAgent || 'banking-js',
+ ofxVersion: options.ofxVersion || null
+ };
+
+ // Recommendations for resolution
+ this.recommendations = options.recommendations || [];
+
+ // Additional metadata
+ this.metadata = options.metadata || {};
+
+ debug('BankingError created:', {
+ name: this.name,
+ code: this.code,
+ correlationId: this.correlationId,
+ category: this.category,
+ retryable: this.retryable
+ });
+ }
+
+ /**
+ * Sanitize URL to remove sensitive query parameters or credentials
+ * @private
+ */
+ _sanitizeUrl(url) {
+ try {
+ const urlObj = new URL(url);
+ // Remove any query parameters that might contain sensitive data
+ urlObj.search = '';
+ // Remove user info if present
+ urlObj.username = '';
+ urlObj.password = '';
+ return urlObj.toString();
+ } catch (e) {
+ return '[INVALID_URL]';
+ }
+ }
+
+ /**
+ * Get a sanitized version of the error for logging (PCI compliant)
+ */
+ toLogObject() {
+ return {
+ name: this.name,
+ message: this.message,
+ code: this.code,
+ correlationId: this.correlationId,
+ timestamp: this.timestamp,
+ category: this.category,
+ retryable: this.retryable,
+ bankingContext: {
+ fid: this.bankingContext.fid,
+ fidOrg: this.bankingContext.fidOrg,
+ operationType: this.bankingContext.operationType,
+ accountType: this.bankingContext.accountType,
+ url: this.bankingContext.url
+ },
+ technicalDetails: {
+ httpStatus: this.technicalDetails.httpStatus,
+ ofxStatus: this.technicalDetails.ofxStatus,
+ requestId: this.technicalDetails.requestId,
+ userAgent: this.technicalDetails.userAgent,
+ ofxVersion: this.technicalDetails.ofxVersion
+ },
+ recommendations: this.recommendations
+ };
+ }
+
+ /**
+ * Get JSON representation of the error
+ */
+ toJSON() {
+ return this.toLogObject();
+ }
+}
+
+/**
+ * Network-related errors
+ */
+class NetworkError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'NETWORK' });
+ this.retryable = options.retryable !== undefined ? options.retryable : true;
+ this.maxRetries = options.maxRetries || 3;
+ }
+}
+
+class ConnectionError extends NetworkError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'CONNECTION_ERROR' });
+ this.recommendations = [
+ 'Check network connectivity',
+ 'Verify firewall settings',
+ 'Ensure the banking server is accessible',
+ 'Try again in a few moments'
+ ];
+ }
+}
+
+class TimeoutError extends NetworkError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'TIMEOUT_ERROR' });
+ this.retryable = true;
+ this.maxRetries = 2; // Fewer retries for timeouts
+ this.recommendations = [
+ 'Increase timeout values for the operation type',
+ 'Check network latency to the banking server',
+ 'Consider reducing the date range for large requests',
+ 'Retry the operation'
+ ];
+ }
+}
+
+class DNSError extends NetworkError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'DNS_ERROR' });
+ this.retryable = false; // DNS errors usually indicate configuration issues
+ this.maxRetries = 0; // DNS errors shouldn't be retried
+ this.recommendations = [
+ 'Verify the banking server URL is correct',
+ 'Check DNS configuration',
+ 'Ensure network connectivity',
+ 'Contact the financial institution for correct server details'
+ ];
+ }
+}
+
+class CertificateError extends NetworkError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'CERTIFICATE_ERROR' });
+ this.retryable = false;
+ this.recommendations = [
+ 'Verify SSL certificate configuration',
+ 'Check if rejectUnauthorized should be disabled (not recommended for production)',
+ 'Ensure system clock is correct',
+ 'Contact the financial institution about certificate issues'
+ ];
+ }
+}
+
+/**
+ * Authentication and authorization errors
+ */
+class AuthenticationError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'AUTHENTICATION' });
+ this.retryable = false; // Authentication errors usually require user intervention
+ }
+}
+
+class InvalidCredentialsError extends AuthenticationError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INVALID_CREDENTIALS' });
+ this.recommendations = [
+ 'Verify username and password are correct',
+ 'Check if account is locked or suspended',
+ 'Ensure account has OFX access enabled',
+ 'Contact the financial institution if credentials are correct'
+ ];
+ }
+}
+
+class ExpiredSessionError extends AuthenticationError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'EXPIRED_SESSION' });
+ this.retryable = true;
+ this.maxRetries = 1;
+ this.recommendations = [
+ 'Reauthenticate with fresh credentials',
+ 'Clear any cached session data',
+ 'Retry the operation with new authentication'
+ ];
+ }
+}
+
+class InsufficientPermissionsError extends AuthenticationError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INSUFFICIENT_PERMISSIONS' });
+ this.recommendations = [
+ 'Verify account has OFX download permissions',
+ 'Check with the financial institution about account access',
+ 'Ensure account type supports the requested operation'
+ ];
+ }
+}
+
+/**
+ * Banking-specific business logic errors
+ */
+class BankingBusinessError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'BANKING_BUSINESS' });
+ }
+}
+
+class AccountNotFoundError extends BankingBusinessError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'ACCOUNT_NOT_FOUND' });
+ this.recommendations = [
+ 'Verify the account ID/number is correct',
+ 'Check if the account type matches the actual account',
+ 'Ensure the account is active and not closed',
+ 'Verify bank routing number (for bank accounts)'
+ ];
+ }
+}
+
+class InsufficientFundsError extends BankingBusinessError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INSUFFICIENT_FUNDS' });
+ this.recommendations = [
+ 'Check account balance',
+ 'Verify transaction amount',
+ 'Consider overdraft protection settings'
+ ];
+ }
+}
+
+class MaintenanceModeError extends BankingBusinessError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'MAINTENANCE_MODE' });
+ this.retryable = true;
+ this.maxRetries = 1;
+ this.retryAfter = options.retryAfter || 3600; // Retry after 1 hour by default
+ this.recommendations = [
+ 'Wait for maintenance window to complete',
+ "Check the financial institution's website for maintenance schedules",
+ 'Retry the operation later',
+ 'Consider using alternative banking channels temporarily'
+ ];
+ }
+}
+
+class DailyLimitExceededError extends BankingBusinessError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'DAILY_LIMIT_EXCEEDED' });
+ this.retryable = true;
+ this.retryAfter = 86400; // Retry after 24 hours
+ this.recommendations = [
+ 'Wait until the next business day',
+ 'Contact the financial institution to increase limits',
+ 'Split large requests into smaller date ranges'
+ ];
+ }
+}
+
+/**
+ * OFX protocol-specific errors
+ */
+class OFXProtocolError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'OFX_PROTOCOL' });
+ }
+}
+
+class MalformedResponseError extends OFXProtocolError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'MALFORMED_RESPONSE' });
+ this.retryable = true;
+ this.maxRetries = 2;
+ this.recommendations = [
+ 'Check OFX version compatibility',
+ 'Verify the financial institution supports the requested OFX version',
+ 'Try with a different OFX version',
+ 'Contact the financial institution about response format issues'
+ ];
+ }
+}
+
+class VersionMismatchError extends OFXProtocolError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'VERSION_MISMATCH' });
+ this.retryable = true;
+ this.maxRetries = 1;
+ this.recommendations = [
+ 'Update to a supported OFX version',
+ "Check the financial institution's supported OFX versions",
+ 'Try with OFX version 1.0.2 or 2.0.3',
+ 'Verify application version compatibility'
+ ];
+ }
+}
+
+class InvalidOFXHeaderError extends OFXProtocolError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INVALID_OFX_HEADER' });
+ this.recommendations = [
+ 'Check OFX header format',
+ 'Verify required header fields are present',
+ 'Ensure proper encoding and character set',
+ 'Validate against OFX specification'
+ ];
+ }
+}
+
+/**
+ * Rate limiting and throttling errors
+ */
+class RateLimitError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'RATE_LIMIT', code: 'RATE_LIMITED' });
+ this.retryable = true;
+ this.maxRetries = 3;
+ this.retryAfter = options.retryAfter || 60; // Default 1 minute
+ this.recommendations = [
+ 'Reduce request frequency',
+ 'Implement exponential backoff',
+ 'Check rate limiting policies with the financial institution',
+ 'Consider batching requests'
+ ];
+ }
+}
+
+class TooManyRequestsError extends RateLimitError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'TOO_MANY_REQUESTS' });
+ this.retryAfter = options.retryAfter || 300; // 5 minutes
+ this.recommendations = [
+ 'Wait before making additional requests',
+ 'Implement request queuing with delays',
+ 'Contact the financial institution about rate limits',
+ 'Spread requests across multiple connections if allowed'
+ ];
+ }
+}
+
+/**
+ * Configuration and setup errors
+ */
+class ConfigurationError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'CONFIGURATION' });
+ this.retryable = false; // Configuration errors require code changes
+ }
+}
+
+class InvalidConfigurationError extends ConfigurationError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INVALID_CONFIGURATION' });
+ this.recommendations = [
+ 'Verify all required configuration parameters',
+ 'Check parameter formats and types',
+ 'Ensure FID and bank ID are correct',
+ 'Validate URL format and accessibility'
+ ];
+ }
+}
+
+class MissingParameterError extends ConfigurationError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'MISSING_PARAMETER' });
+ this.recommendations = [
+ 'Provide all required parameters',
+ 'Check documentation for required fields',
+ 'Verify parameter names are spelled correctly'
+ ];
+ }
+}
+
+/**
+ * Data validation and parsing errors
+ */
+class DataError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, { ...options, category: 'DATA' });
+ }
+}
+
+class InvalidDateRangeError extends DataError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'INVALID_DATE_RANGE' });
+ this.recommendations = [
+ 'Ensure start date is before end date',
+ 'Use YYYYMMDD or YYYYMMDDHHMMSS format',
+ 'Check date range limits imposed by the financial institution',
+ 'Verify dates are not in the future'
+ ];
+ }
+}
+
+class DataParsingError extends DataError {
+ constructor(message, options = {}) {
+ super(message, { ...options, code: 'DATA_PARSING_ERROR' });
+ this.retryable = true;
+ this.maxRetries = 1;
+ this.recommendations = [
+ 'Check data format and encoding',
+ 'Verify character set compatibility',
+ 'Try parsing with different options',
+ 'Contact support if data appears corrupted'
+ ];
+ }
+}
+
+/**
+ * Cache-related errors
+ * Used for cache operations, storage issues, and cache configuration problems
+ */
+class CacheError extends BankingError {
+ constructor(message, options = {}) {
+ super(message, {
+ ...options,
+ code: options.code || 'CACHE_ERROR',
+ category: 'CACHE'
+ });
+ this.retryable = options.retryable !== undefined ? options.retryable : false;
+ this.maxRetries = options.maxRetries || 0;
+ this.recommendations = options.recommendations || [
+ 'Check cache configuration',
+ 'Verify cache storage accessibility',
+ 'Consider disabling cache temporarily',
+ 'Review cache memory limits'
+ ];
+ }
+}
+
+/**
+ * Error factory function to create appropriate error types
+ */
+function createBankingError(errorInfo, options = {}) {
+ const { code, message, httpStatus, originalError } = errorInfo;
+
+ // Map common Node.js error codes to banking error types
+ if (originalError) {
+ const nodeErrorCode = originalError.code;
+ const nodeMessage = originalError.message || message;
+
+ switch (nodeErrorCode) {
+ case 'ENOTFOUND':
+ case 'EAI_NODATA':
+ case 'EAI_NONAME':
+ return new DNSError(nodeMessage, { ...options, originalError });
+
+ case 'ECONNREFUSED':
+ case 'ECONNRESET':
+ case 'ENETUNREACH':
+ case 'EHOSTUNREACH':
+ return new ConnectionError(nodeMessage, { ...options, originalError });
+
+ case 'ETIMEDOUT':
+ case 'ESOCKETTIMEDOUT':
+ return new TimeoutError(nodeMessage, { ...options, originalError });
+
+ case 'CERT_SIGNATURE_FAILURE':
+ case 'CERT_NOT_YET_VALID':
+ case 'CERT_HAS_EXPIRED':
+ case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
+ return new CertificateError(nodeMessage, { ...options, originalError });
+ }
+ }
+
+ // Map HTTP status codes
+ if (httpStatus) {
+ switch (httpStatus) {
+ case 401:
+ return new InvalidCredentialsError(message, { ...options, httpStatus });
+ case 403:
+ return new InsufficientPermissionsError(message, { ...options, httpStatus });
+ case 404:
+ return new AccountNotFoundError(message, { ...options, httpStatus });
+ case 429:
+ return new TooManyRequestsError(message, { ...options, httpStatus });
+ case 503:
+ return new MaintenanceModeError(message, { ...options, httpStatus });
+ }
+ }
+
+ // Map by error code
+ switch (code) {
+ case 'INVALID_CREDENTIALS':
+ return new InvalidCredentialsError(message, options);
+ case 'ACCOUNT_NOT_FOUND':
+ return new AccountNotFoundError(message, options);
+ case 'MALFORMED_RESPONSE':
+ return new MalformedResponseError(message, options);
+ case 'RATE_LIMITED':
+ return new RateLimitError(message, options);
+ case 'INVALID_CONFIGURATION':
+ return new InvalidConfigurationError(message, options);
+ default:
+ return new BankingError(message, { ...options, code });
+ }
+}
+
+// Export all error classes
+module.exports = {
+ // Base error
+ BankingError,
+
+ // Network errors
+ NetworkError,
+ ConnectionError,
+ TimeoutError,
+ DNSError,
+ CertificateError,
+
+ // Authentication errors
+ AuthenticationError,
+ InvalidCredentialsError,
+ ExpiredSessionError,
+ InsufficientPermissionsError,
+
+ // Banking business errors
+ BankingBusinessError,
+ AccountNotFoundError,
+ InsufficientFundsError,
+ MaintenanceModeError,
+ DailyLimitExceededError,
+
+ // OFX protocol errors
+ OFXProtocolError,
+ MalformedResponseError,
+ VersionMismatchError,
+ InvalidOFXHeaderError,
+
+ // Rate limiting errors
+ RateLimitError,
+ TooManyRequestsError,
+
+ // Configuration errors
+ ConfigurationError,
+ InvalidConfigurationError,
+ MissingParameterError,
+
+ // Data errors
+ DataError,
+ InvalidDateRangeError,
+ DataParsingError,
+
+ // Cache errors
+ CacheError,
+
+ // Factory function
+ createBankingError
+};
diff --git a/lib/ofx.js b/lib/ofx.js
index a7bebc1..2876901 100644
--- a/lib/ofx.js
+++ b/lib/ofx.js
@@ -1,49 +1,60 @@
-
/*!
* [OFX description]
* @type {[type]}
*/
-var xml2js = require('xml2js')
- , parser = new xml2js.Parser({explicitArray: false})
- , util = require('./utils')
- , debug = require('debug')('banking:ofx');
+const xml2js = require('xml2js'),
+ parser = new xml2js.Parser({ explicitArray: false }),
+ streamingParser = new xml2js.Parser({
+ explicitArray: false,
+ normalize: false,
+ trim: true,
+ mergeAttrs: true
+ }),
+ util = require('./utils'),
+ debug = require('debug')('banking:ofx');
// expose OFX
-var OFX = module.exports = {};
+const OFX = (module.exports = {});
+
+// Performance optimization: Cache for preprocessed XML patterns
+const _xmlPatternCache = new Map();
+const MAX_CACHE_SIZE = 100; // Limit memory usage
+const CACHE_CLEANUP_THRESHOLD = 80;
function getSignOnMsg(opts) {
- var dtClient = (new Date()).toISOString().substring(0, 20).replace(/[^0-9]/g, '');
-
- return '' +
- '' +
- '' + dtClient +
- '' + opts.user +
- '' + opts.password +
- 'ENG' +
- '' +
- '' + opts.fidOrg +
- '' + opts.fid +
- ' ' +
- '' + opts.app +
- '' + opts.appVer +
- (typeof opts.clientId !== 'undefined' ? '' + opts.clientId : '') +
- ' ' +
- ' ';
+ const dtClient = new Date()
+ .toISOString()
+ .substring(0, 20)
+ .replace(/[^0-9]/g, '');
+
+ return (
+ `` +
+ `` +
+ `${dtClient}${opts.user}${opts.password}ENG` +
+ `` +
+ `${opts.fidOrg}${opts.fid} ` +
+ `${opts.app}${opts.appVer}${
+ typeof opts.clientId !== 'undefined' ? `${opts.clientId}` : ''
+ } ` +
+ ` `
+ );
}
function getOfxHeaders(opts) {
- return 'OFXHEADER:100\r\n' +
- 'DATA:OFXSGML\r\n' +
- 'VERSION:' + opts.ofxVer + '\r\n' +
- 'SECURITY:NONE\r\n' +
- 'ENCODING:USASCII\r\n' +
- 'CHARSET:1252\r\n' +
- 'COMPRESSION:NONE\r\n' +
- 'OLDFILEUID:NONE\r\n' +
- 'NEWFILEUID:' + util.uuid(32) + '\r\n' +
- '\r\n';
+ return (
+ `OFXHEADER:100\r\n` +
+ `DATA:OFXSGML\r\n` +
+ `VERSION:${opts.ofxVer}\r\n` +
+ `SECURITY:NONE\r\n` +
+ `ENCODING:USASCII\r\n` +
+ `CHARSET:1252\r\n` +
+ `COMPRESSION:NONE\r\n` +
+ `OLDFILEUID:NONE\r\n` +
+ `NEWFILEUID:${util.uuid(32)}\r\n` +
+ `\r\n`
+ );
}
/**
@@ -52,16 +63,16 @@ function getOfxHeaders(opts) {
* @returns {string}
*/
OFX.buildAccountListRequest = function (opts) {
- var reqStr = getOfxHeaders(opts) + '' + getSignOnMsg(opts);
- reqStr += '' +
- '' +
- '' + util.uuid(32) +
- '' +
- '19900101' +
- ' ' +
- ' ' +
- ' ' +
- ' ';
+ let reqStr = `${getOfxHeaders(opts)}${getSignOnMsg(opts)}`;
+ reqStr +=
+ `` +
+ `` +
+ `${util.uuid(32)}` +
+ `19900101` +
+ ` ` +
+ ` ` +
+ ` ` +
+ ` `;
return reqStr;
};
@@ -72,70 +83,55 @@ OFX.buildAccountListRequest = function (opts) {
* @returns {string}
*/
OFX.buildStatementRequest = function (opts) {
- var type = (opts.accType || '').toUpperCase();
- var reqStr = getOfxHeaders(opts) + '' + getSignOnMsg(opts);
+ const type = (opts.accType || '').toUpperCase();
+ let reqStr = `${getOfxHeaders(opts)}${getSignOnMsg(opts)}`;
switch (type) {
case 'INVESTMENT':
- reqStr += '' +
- '' +
- '' + util.uuid(32) +
- '' + util.uuid(5) +
- '' +
- '' +
- '' + opts.brokerId +
- '' + opts.accId +
- ' ' +
- '' +
- '' + opts.start +
- (typeof opts.end !== 'undefined' ? '' + opts.end : '') +
- 'Y ' +
- 'Y' +
- '' +
- 'Y' +
- ' ' +
- 'Y' +
- ' ' +
- ' ' +
- ' ';
+ reqStr +=
+ `` +
+ `` +
+ `${util.uuid(32)}${util.uuid(5)}` +
+ `` +
+ `${opts.brokerId}${opts.accId} ` +
+ `` +
+ `${opts.start}${typeof opts.end !== 'undefined' ? `${opts.end}` : ''}Y ` +
+ `Y` +
+ `` +
+ `Y` +
+ ` ` +
+ `Y` +
+ ` ` +
+ ` ` +
+ ` `;
break;
case 'CREDITCARD':
- reqStr += '' +
- '' +
- '' + util.uuid(32) +
- '' + util.uuid(5) +
- '' +
- '' +
- '' + opts.accId +
- ' ' +
- '' +
- '' + opts.start +
- (typeof opts.end !== 'undefined' ? '' + opts.end : '') +
- 'Y ' +
- ' ' +
- ' ' +
- ' ';
+ reqStr +=
+ `` +
+ `` +
+ `${util.uuid(32)}${util.uuid(5)}` +
+ `` +
+ `${opts.accId} ` +
+ `` +
+ `${opts.start}${typeof opts.end !== 'undefined' ? `${opts.end}` : ''}Y ` +
+ ` ` +
+ ` ` +
+ ` `;
break;
default:
- reqStr += '' +
- '' +
- '' + util.uuid(32) +
- '' + util.uuid(5) +
- '' +
- '' +
- '' + opts.bankId +
- '' + opts.accId +
- '' + type +
- ' ' +
- '' +
- '' + opts.start +
- (typeof opts.end !== 'undefined' ? '' + opts.end : '') +
- 'Y ' +
- ' ' +
- ' ' +
- ' ';
+ reqStr +=
+ `` +
+ `` +
+ `${util.uuid(32)}${util.uuid(5)}` +
+ `` +
+ `${opts.bankId}${opts.accId}${type} ` +
+ `` +
+ `${opts.start}${typeof opts.end !== 'undefined' ? `${opts.end}` : ''}Y ` +
+ ` ` +
+ ` ` +
+ ` `;
}
reqStr += ' ';
@@ -145,18 +141,132 @@ OFX.buildStatementRequest = function (opts) {
};
/**
- * Parse an OFX response string
+ * Parse an OFX response string - Performance Optimized
* @param ofxStr
* @param fn
*/
OFX.parse = function (ofxStr, fn) {
- var data = {};
- var callback = fn;
- var ofxRes = ofxStr.split('', 2);
- var ofx = '' + ofxRes[1];
- var headerString = ofxRes[0].split(/\r|\n/);
+ const parseStartTime = process.hrtime.bigint();
+ const initialMemory = process.memoryUsage();
+
+ debug(`OFX Parse Start: ${ofxStr.length} bytes, Memory: ${Math.round(initialMemory.heapUsed / 1024 / 1024)}MB`);
+
+ const data = {};
+ const ofxRes = ofxStr.split('', 2);
+ const ofx = `${ofxRes[1]}`;
+ const headerString = ofxRes[0].split(/\r|\n/);
+
+ // Auto-detect if streaming parsing should be used for large responses
+ const shouldUseStreaming = _shouldUseStreamingParse(ofxStr);
+
+ if (shouldUseStreaming) {
+ debug('Using streaming parser for large OFX response');
+ return _parseWithStreaming(ofx, headerString, fn, parseStartTime, initialMemory);
+ }
+
+ // Performance optimization: Single-pass XML preprocessing
+ // Combined multiple regex operations into one optimized function
+ data.xml = _optimizedXmlPreprocess(ofx);
+
+ parser.parseString(data.xml, (err, result) => {
+ if (err) {
+ debug('XML Parsing Error:', err);
+ }
+ data.body = result;
+ });
+
+ // Performance optimization: Optimized header parsing
+ data.header = _optimizedHeaderParse(headerString);
+
+ // Log performance metrics
+ _logParseMetrics(parseStartTime, initialMemory, ofxStr.length, shouldUseStreaming);
+
+ fn(data);
+};
+
+/**
+ * Determine if streaming parsing should be used based on content size and complexity
+ * @private
+ */
+function _shouldUseStreamingParse(ofxStr) {
+ // Use streaming for responses larger than 1MB
+ if (ofxStr.length > 1024 * 1024) return true;
- data.xml = ofx
+ // Use streaming if many transaction records detected
+ const transactionCount = (ofxStr.match(//g) || []).length;
+ if (transactionCount > 1000) return true;
+
+ return false;
+}
+
+/**
+ * Parse large OFX responses using streaming approach to minimize memory usage
+ * @private
+ */
+function _parseWithStreaming(ofx, headerString, fn, parseStartTime, initialMemory) {
+ const data = {
+ header: _optimizedHeaderParse(headerString),
+ body: null,
+ xml: null
+ };
+
+ // For streaming, we still need to preprocess but with memory optimization
+ const processedXml = _optimizedXmlPreprocessStreaming(ofx);
+ data.xml = processedXml;
+
+ streamingParser.parseString(processedXml, (err, result) => {
+ if (err) {
+ debug('Streaming XML Parsing Error:', err);
+ }
+ data.body = result;
+
+ // Log streaming performance metrics
+ _logParseMetrics(parseStartTime, initialMemory, ofx.length, true);
+
+ fn(data);
+ });
+}
+
+/**
+ * Memory-optimized XML preprocessing for streaming
+ * @private
+ */
+function _optimizedXmlPreprocessStreaming(ofx) {
+ // For very large files, process in chunks to avoid memory spikes
+ const chunkSize = 64 * 1024; // 64KB chunks
+ let result = '';
+
+ for (let i = 0; i < ofx.length; i += chunkSize) {
+ const chunk = ofx.slice(i, i + chunkSize);
+ result += _optimizedXmlPreprocess(chunk);
+
+ // Allow event loop to process other operations
+ if (i % (chunkSize * 10) === 0) {
+ // Force garbage collection opportunity for large files
+ if (global.gc) {
+ global.gc();
+ }
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Optimized XML preprocessing with combined regex operations and caching
+ * Uses the original proven logic but with performance optimizations
+ * @private
+ */
+function _optimizedXmlPreprocess(ofx) {
+ // Check cache first for small, common patterns
+ const cacheKey = _generateCacheKey(ofx);
+ if (cacheKey && _xmlPatternCache.has(cacheKey)) {
+ debug('XML preprocessing cache hit');
+ return _xmlPatternCache.get(cacheKey);
+ }
+
+ // Original proven logic with performance optimizations
+ let result = ofx
// Remove empty spaces and line breaks between tags
.replace(/>\s+<')
// Remove empty spaces and line breaks before tags content
@@ -164,23 +274,100 @@ OFX.parse = function (ofxStr, fn) {
// Remove empty spaces and line breaks after tags content
.replace(/>\s+/g, '>')
// Remove dots in start-tags names and remove end-tags with dots
- .replace(/<([A-Z0-9_]*)+\.+([A-Z0-9_]*)>([^<]+)(<\/\1\.\2>)?/g, '<\$1\$2>\$3')
+ .replace(/<([A-Z0-9_]*)+\.+([A-Z0-9_]*)>([^<]+)(<\/\1\.\2>)?/g, '<$1$2>$3')
// Add a new end-tags for the ofx elements
- .replace(/<(\w+?)>([^<]+)/g, '<\$1>\$2\$1>')
+ .replace(/<(\w+?)>([^<]+)/g, '<$1>$2$1>')
// Remove duplicate end-tags
- .replace(/<\/(\w+?)>(<\/\1>)?/g, '\$1>');
+ .replace(/<\/(\w+?)>(<\/\1>)?/g, '$1>');
- parser.parseString(data.xml, function (err, result) {
- data.body = result;
- });
+ // Cache result if it's worth caching (small, common patterns)
+ if (cacheKey && ofx.length < 1024) {
+ _cacheXmlPattern(cacheKey, result);
+ }
- data.header = {};
+ return result;
+}
+
+/**
+ * Generate cache key for XML patterns
+ * @private
+ */
+function _generateCacheKey(ofx) {
+ // Only cache small, common patterns to avoid memory bloat
+ if (ofx.length > 1024) return null;
+
+ // Create hash-like key from first/last chars and length
+ const start = ofx.substring(0, 20);
+ const end = ofx.substring(Math.max(0, ofx.length - 20));
+ return `${start.length}_${end.length}_${ofx.length}`;
+}
- for (var key in headerString) {
- if (typeof headerString[key] === "string") {
- var headAttributes = headerString[key].split(/:/, 2);
+/**
+ * Cache XML pattern with LRU-style cleanup
+ * @private
+ */
+function _cacheXmlPattern(key, result) {
+ if (_xmlPatternCache.size >= MAX_CACHE_SIZE) {
+ // Simple LRU: remove oldest entries
+ const keysToDelete = Array.from(_xmlPatternCache.keys()).slice(0, MAX_CACHE_SIZE - CACHE_CLEANUP_THRESHOLD);
+ for (const oldKey of keysToDelete) {
+ _xmlPatternCache.delete(oldKey);
}
- if (headAttributes[0]) data.header[headAttributes[0]] = headAttributes[1];
+ debug(`XML cache cleanup: removed ${keysToDelete.length} entries`);
}
- fn(data);
-};
+
+ _xmlPatternCache.set(key, result);
+}
+
+/**
+ * Log performance metrics for OFX parsing operations
+ * @private
+ */
+function _logParseMetrics(parseStartTime, initialMemory, inputSize, usedStreaming) {
+ const parseEndTime = process.hrtime.bigint();
+ const finalMemory = process.memoryUsage();
+
+ const durationMs = Number(parseEndTime - parseStartTime) / 1000000; // Convert nanoseconds to milliseconds
+ const memoryDeltaMB = Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024);
+ const throughputMBps = inputSize / 1024 / 1024 / (durationMs / 1000);
+
+ debug(`OFX Parse Complete:
+ Duration: ${durationMs.toFixed(2)}ms
+ Input Size: ${(inputSize / 1024).toFixed(1)}KB
+ Memory Delta: ${memoryDeltaMB >= 0 ? '+' : ''}${memoryDeltaMB}MB
+ Throughput: ${throughputMBps.toFixed(2)}MB/s
+ Streaming: ${usedStreaming ? 'Yes' : 'No'}
+ Cache Hits: ${_xmlPatternCache.size}`);
+
+ // Warn if memory usage is excessive
+ if (memoryDeltaMB > 100) {
+ debug(`WARNING: High memory usage detected (+${memoryDeltaMB}MB). Consider increasing streaming threshold.`);
+ }
+
+ // Warn if performance is poor
+ if (throughputMBps < 1.0) {
+ debug(`WARNING: Low throughput detected (${throughputMBps.toFixed(2)}MB/s). Performance may be degraded.`);
+ }
+}
+
+/**
+ * Optimized header parsing with better error handling
+ * @private
+ */
+function _optimizedHeaderParse(headerString) {
+ const headers = {};
+
+ for (let i = 0; i < headerString.length; i++) {
+ const line = headerString[i];
+ if (typeof line === 'string' && line.includes(':')) {
+ const colonIndex = line.indexOf(':');
+ const key = line.substring(0, colonIndex).trim();
+ const value = line.substring(colonIndex + 1).trim();
+ if (key) {
+ headers[key] = value;
+ }
+ }
+ }
+
+ return headers;
+}
diff --git a/lib/retry-manager.js b/lib/retry-manager.js
new file mode 100644
index 0000000..41a7a4c
--- /dev/null
+++ b/lib/retry-manager.js
@@ -0,0 +1,471 @@
+/*!
+ * Retry Manager for banking.js
+ * Provides intelligent retry logic with exponential backoff and jitter
+ * Optimized for banking operations and various failure types
+ */
+
+const debug = require('debug')('banking:retry');
+
+// Constants
+const STRINGIFY_SPACE = 2;
+
+/**
+ * Default retry configuration optimized for banking operations
+ */
+const defaultRetryConfig = {
+ // Maximum retry attempts for different operation types
+ maxRetries: {
+ quick: 3, // Account validation, balance checks
+ standard: 5, // Statement downloads, transaction lists
+ heavy: 2 // Large date range downloads
+ },
+
+ // Base delay in milliseconds for exponential backoff
+ baseDelay: 1000,
+
+ // Maximum delay cap to prevent excessively long waits
+ maxDelay: 30000,
+
+ // Backoff strategies
+ backoffStrategy: 'exponential', // 'exponential', 'linear', 'fixed'
+
+ // Jitter configuration to prevent thundering herd
+ jitter: {
+ enabled: true,
+ type: 'full', // 'full', 'equal', 'decorrelated'
+ factor: 0.1 // Jitter factor (0.0 to 1.0)
+ },
+
+ // Timeout configuration for different operation types
+ timeouts: {
+ quick: {
+ connection: 5000, // 5 seconds for connection establishment
+ request: 15000, // 15 seconds total request timeout
+ socket: 10000 // 10 seconds socket timeout
+ },
+ standard: {
+ connection: 10000, // 10 seconds for connection establishment
+ request: 60000, // 60 seconds total request timeout
+ socket: 30000 // 30 seconds socket timeout
+ },
+ heavy: {
+ connection: 15000, // 15 seconds for connection establishment
+ request: 120000, // 2 minutes total request timeout
+ socket: 60000 // 60 seconds socket timeout
+ }
+ },
+
+ // Retry conditions - which errors should trigger retries
+ retryConditions: {
+ // Network-level errors that are typically transient
+ networkErrors: [
+ 'ECONNRESET', // Connection reset by peer
+ 'ETIMEDOUT', // Connection/request timeout
+ 'ECONNREFUSED', // Connection refused (server down)
+ 'ENOTFOUND', // DNS resolution failure
+ 'ENETUNREACH', // Network unreachable
+ 'EHOSTUNREACH', // Host unreachable
+ 'EPIPE', // Broken pipe
+ 'ECONNABORTED' // Connection aborted
+ ],
+
+ // HTTP status codes that warrant retries
+ httpStatusCodes: [
+ 408, // Request Timeout
+ 429, // Too Many Requests (rate limiting)
+ 500, // Internal Server Error
+ 502, // Bad Gateway
+ 503, // Service Unavailable
+ 504, // Gateway Timeout
+ 507, // Insufficient Storage
+ 520, // Unknown Error (Cloudflare)
+ 521, // Web Server Is Down (Cloudflare)
+ 522, // Connection Timed Out (Cloudflare)
+ 523, // Origin Is Unreachable (Cloudflare)
+ 524 // A Timeout Occurred (Cloudflare)
+ ],
+
+ // SSL/TLS errors that might be transient
+ sslErrors: [
+ 'EPROTO', // SSL protocol error
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', // Certificate verification failed
+ 'CERT_HAS_EXPIRED', // Certificate expired
+ 'SSL_HANDSHAKE_FAILURE' // SSL handshake failed
+ ],
+
+ // Banking-specific OFX error codes that should NOT be retried
+ nonRetryableOFXCodes: [
+ '15500', // Invalid user credentials
+ '15501', // Customer account already in use
+ '15502', // Invalid user ID
+ '15503', // Invalid password
+ '15505', // Password expired
+ '15510', // Account suspended
+ '10500', // Invalid account number
+ '10401', // Account restricted
+ '2020', // Invalid date range
+ '10015' // Unsupported OFX version
+ ]
+ },
+
+ // Rate limiting configuration
+ rateLimiting: {
+ enabled: true,
+ maxConcurrent: 3, // Maximum concurrent requests per host
+ requestInterval: 500 // Minimum time between requests (ms)
+ }
+};
+
+/**
+ * RetryManager class for handling intelligent retry logic
+ */
+function RetryManager(config) {
+ if (!(this instanceof RetryManager)) return new RetryManager(config);
+
+ this.config = this._mergeConfig(defaultRetryConfig, config || {});
+ this.metrics = {
+ totalAttempts: 0,
+ successfulRetries: 0,
+ failedRetries: 0,
+ timeouts: 0,
+ networkErrors: 0,
+ httpErrors: 0,
+ sslErrors: 0,
+ ofxErrors: 0,
+ averageAttempts: 0,
+ totalDelay: 0
+ };
+
+ // Rate limiting state per host
+ this.rateLimitState = new Map();
+
+ debug('RetryManager initialized with config:', JSON.stringify(this.config, null, STRINGIFY_SPACE));
+}
+
+/**
+ * Deep merge configuration objects
+ * @param {object} defaultConfig - Default configuration
+ * @param {object} userConfig - User-provided configuration
+ * @returns {object} Merged configuration
+ */
+RetryManager.prototype._mergeConfig = function (defaultConfig, userConfig) {
+ const merged = JSON.parse(JSON.stringify(defaultConfig));
+
+ function deepMerge(target, source) {
+ for (const key in source) {
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
+ target[key] = target[key] || {};
+ deepMerge(target[key], source[key]);
+ } else {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ deepMerge(merged, userConfig);
+ return merged;
+};
+
+/**
+ * Determine if an error should trigger a retry
+ * @param {Error} error - The error that occurred
+ * @param {number} attempt - Current attempt number
+ * @param {string} operationType - Type of operation (quick, standard, heavy)
+ * @returns {boolean} Whether the error should be retried
+ */
+RetryManager.prototype.shouldRetry = function (error, attempt, operationType = 'standard') {
+ const maxRetries = this.config.maxRetries[operationType] || this.config.maxRetries.standard;
+
+ // Don't retry if we've exceeded max attempts
+ if (attempt >= maxRetries) {
+ debug(`Max retries (${maxRetries}) exceeded for ${operationType} operation`);
+ return false;
+ }
+
+ // Check for network errors
+ if (error.code && this.config.retryConditions.networkErrors.includes(error.code)) {
+ debug(`Network error detected: ${error.code}, will retry`);
+ this.metrics.networkErrors++;
+ return true;
+ }
+
+ // Check for HTTP status codes
+ if (error.statusCode && this.config.retryConditions.httpStatusCodes.includes(error.statusCode)) {
+ debug(`HTTP error detected: ${error.statusCode}, will retry`);
+ this.metrics.httpErrors++;
+ return true;
+ }
+
+ // Check for SSL errors
+ if (error.code && this.config.retryConditions.sslErrors.includes(error.code)) {
+ debug(`SSL error detected: ${error.code}, will retry`);
+ this.metrics.sslErrors++;
+ return true;
+ }
+
+ // Check for timeout errors
+ if (error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
+ debug('Timeout error detected, will retry');
+ this.metrics.timeouts++;
+ return true;
+ }
+
+ // Check for OFX-specific errors that should not be retried
+ if (error.ofxCode && this.config.retryConditions.nonRetryableOFXCodes.includes(error.ofxCode)) {
+ debug(`Non-retryable OFX error: ${error.ofxCode}, will not retry`);
+ this.metrics.ofxErrors++;
+ return false;
+ }
+
+ // Default: don't retry unknown errors
+ debug(`Unknown error type: ${error.code || error.message}, will not retry`);
+ return false;
+};
+
+/**
+ * Calculate delay for next retry attempt with jitter
+ * @param {number} attempt - Current attempt number (0-based)
+ * @param {string} operationType - Type of operation
+ * @returns {number} Delay in milliseconds
+ */
+RetryManager.prototype.calculateDelay = function (attempt, operationType = 'standard') {
+ let delay;
+
+ switch (this.config.backoffStrategy) {
+ case 'exponential':
+ delay = this.config.baseDelay * Math.pow(2, attempt);
+ break;
+ case 'linear':
+ delay = this.config.baseDelay * (attempt + 1);
+ break;
+ case 'fixed':
+ default:
+ delay = this.config.baseDelay;
+ break;
+ }
+
+ // Apply maximum delay cap
+ delay = Math.min(delay, this.config.maxDelay);
+
+ // Apply jitter if enabled
+ if (this.config.jitter.enabled) {
+ delay = this._applyJitter(delay, attempt);
+ }
+
+ this.metrics.totalDelay += delay;
+ debug(`Calculated retry delay: ${delay}ms for attempt ${attempt + 1}`);
+ return delay;
+};
+
+/**
+ * Apply jitter to delay value
+ * @param {number} delay - Base delay in milliseconds
+ * @param {number} attempt - Current attempt number
+ * @returns {number} Jittered delay
+ */
+RetryManager.prototype._applyJitter = function (delay, attempt) {
+ const jitterConfig = this.config.jitter;
+ const JITTER_MULTIPLIER = 2;
+ const DECORRELATED_MULTIPLIER = 3;
+
+ switch (jitterConfig.type) {
+ case 'full': {
+ // Full jitter: random between 0 and delay
+ return Math.random() * delay;
+ }
+ case 'equal': {
+ // Equal jitter: delay/2 + random(0, delay/2)
+ const halfDelay = delay / 2;
+ return halfDelay + Math.random() * halfDelay;
+ }
+ case 'decorrelated': {
+ // Decorrelated jitter: exponential with randomness
+ const base = this.config.baseDelay;
+ return Math.random() * Math.min(this.config.maxDelay, base * DECORRELATED_MULTIPLIER * Math.pow(2, attempt));
+ }
+ default: {
+ // Simple jitter factor
+ const jitterAmount = delay * jitterConfig.factor;
+ return delay + (Math.random() * JITTER_MULTIPLIER - 1) * jitterAmount;
+ }
+ }
+};
+
+/**
+ * Get timeout configuration for operation type
+ * @param {string} operationType - Type of operation (quick, standard, heavy)
+ * @returns {object} Timeout configuration
+ */
+RetryManager.prototype.getTimeoutConfig = function (operationType = 'standard') {
+ return this.config.timeouts[operationType] || this.config.timeouts.standard;
+};
+
+/**
+ * Check rate limiting for a host
+ * @param {string} hostname - Target hostname
+ * @returns {number} Delay needed for rate limiting (0 if none)
+ */
+RetryManager.prototype.checkRateLimit = function (hostname) {
+ if (!this.config.rateLimiting.enabled) {
+ return 0;
+ }
+
+ const now = Date.now();
+ const hostState = this.rateLimitState.get(hostname) || {
+ lastRequest: 0,
+ activeRequests: 0
+ };
+
+ // Check concurrent request limit
+ if (hostState.activeRequests >= this.config.rateLimiting.maxConcurrent) {
+ debug(`Rate limit: max concurrent requests reached for ${hostname}`);
+ return this.config.rateLimiting.requestInterval;
+ }
+
+ // Check minimum interval between requests
+ const timeSinceLastRequest = now - hostState.lastRequest;
+ if (timeSinceLastRequest < this.config.rateLimiting.requestInterval) {
+ const delay = this.config.rateLimiting.requestInterval - timeSinceLastRequest;
+ debug(`Rate limit: enforcing ${delay}ms delay for ${hostname}`);
+ return delay;
+ }
+
+ return 0;
+};
+
+/**
+ * Record request start for rate limiting
+ * @param {string} hostname - Target hostname
+ */
+RetryManager.prototype.recordRequestStart = function (hostname) {
+ if (!this.config.rateLimiting.enabled) {
+ return;
+ }
+
+ const now = Date.now();
+ const hostState = this.rateLimitState.get(hostname) || {
+ lastRequest: 0,
+ activeRequests: 0
+ };
+
+ hostState.lastRequest = now;
+ hostState.activeRequests++;
+ this.rateLimitState.set(hostname, hostState);
+};
+
+/**
+ * Record request end for rate limiting
+ * @param {string} hostname - Target hostname
+ */
+RetryManager.prototype.recordRequestEnd = function (hostname) {
+ if (!this.config.rateLimiting.enabled) {
+ return;
+ }
+
+ const hostState = this.rateLimitState.get(hostname);
+ if (hostState && hostState.activeRequests > 0) {
+ hostState.activeRequests--;
+ this.rateLimitState.set(hostname, hostState);
+ }
+};
+
+/**
+ * Update retry metrics
+ * @param {boolean} success - Whether the retry was successful
+ * @param {number} totalAttempts - Total number of attempts made
+ */
+RetryManager.prototype.updateMetrics = function (success, totalAttempts) {
+ this.metrics.totalAttempts += totalAttempts;
+
+ if (success && totalAttempts > 1) {
+ this.metrics.successfulRetries++;
+ } else if (!success && totalAttempts > 1) {
+ this.metrics.failedRetries++;
+ }
+
+ // Update average attempts
+ const totalOperations = this.metrics.successfulRetries + this.metrics.failedRetries + (totalAttempts === 1 ? 1 : 0);
+ if (totalOperations > 0) {
+ this.metrics.averageAttempts = this.metrics.totalAttempts / totalOperations;
+ }
+};
+
+/**
+ * Get current retry metrics
+ * @returns {object} Current metrics
+ */
+RetryManager.prototype.getMetrics = function () {
+ return Object.assign({}, this.metrics, {
+ retrySuccessRate:
+ this.metrics.successfulRetries / Math.max(1, this.metrics.successfulRetries + this.metrics.failedRetries),
+ averageDelay: this.metrics.totalDelay / Math.max(1, this.metrics.totalAttempts - 1)
+ });
+};
+
+/**
+ * Reset retry metrics
+ */
+RetryManager.prototype.resetMetrics = function () {
+ this.metrics = {
+ totalAttempts: 0,
+ successfulRetries: 0,
+ failedRetries: 0,
+ timeouts: 0,
+ networkErrors: 0,
+ httpErrors: 0,
+ sslErrors: 0,
+ ofxErrors: 0,
+ averageAttempts: 0,
+ totalDelay: 0
+ };
+ debug('Retry metrics reset');
+};
+
+/**
+ * Create a retry wrapper function for any async operation
+ * @param {function} operation - The operation to retry
+ * @param {object} options - Retry options
+ * @returns {Promise} Promise that resolves with operation result
+ */
+RetryManager.prototype.executeWithRetry = function (operation, options = {}) {
+ const self = this;
+ const operationType = options.operationType || 'standard';
+ const maxRetries = this.config.maxRetries[operationType] || this.config.maxRetries.standard;
+
+ return new Promise((resolve, reject) => {
+ let attempt = 0;
+
+ function tryOperation() {
+ attempt++;
+ self.metrics.totalAttempts++;
+
+ debug(`Executing operation attempt ${attempt}/${maxRetries + 1}`);
+
+ Promise.resolve(operation(attempt))
+ .then(result => {
+ debug(`Operation succeeded on attempt ${attempt}`);
+ self.updateMetrics(true, attempt);
+ return resolve(result);
+ })
+ .catch(error => {
+ debug(`Operation failed on attempt ${attempt}:`, error.message);
+
+ if (self.shouldRetry(error, attempt - 1, operationType)) {
+ const delay = self.calculateDelay(attempt - 1, operationType);
+ debug(`Retrying operation in ${delay}ms`);
+
+ setTimeout(tryOperation, delay);
+ } else {
+ debug(`Not retrying operation after ${attempt} attempts`);
+ self.updateMetrics(false, attempt);
+ reject(error);
+ }
+ });
+ }
+
+ tryOperation();
+ });
+};
+
+// Export the RetryManager class
+module.exports = RetryManager;
diff --git a/lib/utils.js b/lib/utils.js
index 8d675ee..a4edf92 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,5 +1,14 @@
-var tls = require('tls');
-var url = require('url');
+const tls = require('tls');
+const url = require('url');
+const ConnectionPool = require('./connection-pool');
+const RetryManager = require('./retry-manager');
+const CacheManager = require('./cache-manager');
+const debug = require('debug')('banking:utils');
+
+// Constants
+const STRINGIFY_SPACE = 2;
+const HTTPS_PORT = 443;
+const HTTP_PORT = 80;
/**
* Unique Id Generator
@@ -10,29 +19,253 @@ var url = require('url');
* @api private
*/
-var Util = module.exports = {};
+const Util = (module.exports = {});
+
+// Global connection pool instance
+let globalPool = null;
+// Global retry manager instance
+let globalRetryManager = null;
+// Global cache manager instance
+let globalCacheManager = null;
+
+/**
+ * Configure connection pooling and retry settings for banking operations
+ * @param {object} config - Combined pool and retry configuration options
+ * @param {object} config.pool - Connection pool specific settings
+ * @param {object} config.retry - Retry manager specific settings
+ * @param {object} config.timeouts - Operation-specific timeout configurations
+ * @returns {object} Applied configuration
+ */
+Util.configurePool = function (config) {
+ debug('Configuring connection pool with config:', JSON.stringify(config, null, STRINGIFY_SPACE));
+
+ if (globalPool) {
+ globalPool.destroy();
+ }
+
+ // Handle both old and new API formats
+ let poolConfig;
+ if (config && (config.pool || config.retry || config.timeouts)) {
+ // New API format with structured config
+ poolConfig = Object.assign({}, config.pool || {});
+ if (config.retry) {
+ poolConfig.retryManager = config.retry;
+ }
+ if (config.timeouts) {
+ poolConfig.timeouts = config.timeouts;
+ }
+ } else {
+ // Old API format - direct pool config
+ poolConfig = Object.assign({}, config || {});
+ }
+
+ globalPool = new ConnectionPool(poolConfig);
+
+ // Return appropriate format based on input
+ if (config && (config.pool || config.retry || config.timeouts)) {
+ return {
+ pool: globalPool.config,
+ retry: globalPool.retryManager ? globalPool.retryManager.config : null
+ };
+ } else {
+ // Backward compatibility - return the pool config directly
+ return globalPool.config;
+ }
+};
+
+/**
+ * Configure retry policies for banking operations
+ * @param {object} config - Retry configuration options
+ * @returns {object} Applied retry configuration
+ */
+Util.configureRetry = function (config) {
+ debug('Configuring retry manager with config:', JSON.stringify(config, null, STRINGIFY_SPACE));
+
+ if (globalRetryManager) {
+ globalRetryManager.resetMetrics();
+ }
+
+ globalRetryManager = new RetryManager(config);
+
+ // If pool exists, update its retry manager
+ if (globalPool) {
+ globalPool.retryManager = globalRetryManager;
+ }
+
+ return globalRetryManager.config;
+};
+
+/**
+ * Configure timeout settings for different operation types
+ * @param {object} timeouts - Timeout configuration by operation type
+ * @returns {object} Applied timeout configuration
+ */
+Util.configureTimeouts = function (timeouts) {
+ debug('Configuring timeouts:', JSON.stringify(timeouts, null, STRINGIFY_SPACE));
+
+ if (!globalPool) {
+ globalPool = new ConnectionPool();
+ }
+
+ // Update timeout configuration
+ Object.assign(globalPool.config.timeouts, timeouts);
+
+ return globalPool.config.timeouts;
+};
+
+/**
+ * Get connection pool metrics including retry metrics
+ * @returns {object} Current pool metrics
+ */
+Util.getPoolMetrics = function () {
+ if (!globalPool) return null;
+ return globalPool.getMetrics();
+};
+
+/**
+ * Get retry metrics
+ * @returns {object} Current retry metrics or null
+ */
+Util.getRetryMetrics = function () {
+ if (globalPool && globalPool.retryManager) {
+ return globalPool.retryManager.getMetrics();
+ }
+ if (globalRetryManager) {
+ return globalRetryManager.getMetrics();
+ }
+ return null;
+};
+
+/**
+ * Reset retry metrics
+ */
+Util.resetRetryMetrics = function () {
+ if (globalPool && globalPool.retryManager) {
+ globalPool.retryManager.resetMetrics();
+ }
+ if (globalRetryManager) {
+ globalRetryManager.resetMetrics();
+ }
+};
+
+/**
+ * Configure caching for banking operations
+ * @param {object} config - Cache configuration options
+ * @returns {object} Applied cache configuration
+ */
+Util.configureCache = function (config) {
+ debug('Configuring cache manager with config:', JSON.stringify(config, null, STRINGIFY_SPACE));
+
+ if (globalCacheManager) {
+ globalCacheManager.destroy();
+ }
+
+ globalCacheManager = new CacheManager(config);
+ return globalCacheManager.config;
+};
+
+/**
+ * Get cache metrics and statistics
+ * @returns {object} Current cache metrics or null if caching is not enabled
+ */
+Util.getCacheMetrics = function () {
+ if (!globalCacheManager) return null;
+ return globalCacheManager.getMetrics();
+};
+
+/**
+ * Reset cache metrics (useful for testing or monitoring)
+ */
+Util.resetCacheMetrics = function () {
+ if (globalCacheManager) {
+ globalCacheManager.resetMetrics();
+ }
+};
+
+/**
+ * Clear all cached data
+ * @returns {number} Number of entries cleared
+ */
+Util.clearCache = function () {
+ if (!globalCacheManager) return 0;
+ return globalCacheManager.clear();
+};
+
+/**
+ * Invalidate cache entries for specific operation
+ * @param {string} operation - Operation type to invalidate
+ * @param {object} [params] - Specific parameters to invalidate (optional)
+ * @returns {number} Number of entries invalidated
+ */
+Util.invalidateCache = function (operation, params = null) {
+ if (!globalCacheManager) return 0;
+ return globalCacheManager.invalidate(operation, params);
+};
+
+/**
+ * Destroy the connection pool, retry manager, and cache manager, clean up resources
+ */
+Util.destroyPool = function () {
+ if (globalPool) {
+ globalPool.destroy();
+ globalPool = null;
+ }
+ if (globalRetryManager) {
+ globalRetryManager.resetMetrics();
+ globalRetryManager = null;
+ }
+ if (globalCacheManager) {
+ globalCacheManager.destroy();
+ globalCacheManager = null;
+ }
+};
+
+/**
+ * Get or create the global connection pool
+ * @returns {ConnectionPool} The global pool instance
+ */
+function getPool() {
+ if (!globalPool) {
+ debug('Creating default connection pool');
+ globalPool = new ConnectionPool();
+ }
+ return globalPool;
+}
+
+/**
+ * Get or create the global cache manager
+ * @returns {CacheManager} The global cache manager instance
+ */
+function getCacheManager() {
+ if (!globalCacheManager) {
+ debug('Creating default cache manager');
+ globalCacheManager = new CacheManager();
+ }
+ return globalCacheManager;
+}
-Util.uuid = function(len,radix) {
- var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
- var chars = CHARS, uuid = [];
- radix = radix || chars.length;
+Util.uuid = function (len, radix) {
+ const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
+ const chars = CHARS,
+ uuid = [];
+ radix = radix || chars.length;
- if (len) {
- for (var i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
- } else {
- var r;
- uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
- uuid[14] = '4';
+ if (len) {
+ for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
+ } else {
+ let r;
+ uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
+ uuid[14] = '4';
- for (var i = 0; i < 36; i++) {
- if (!uuid[i]) {
- r = 0 | Math.random()*16;
- uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
- }
+ for (let j = 0; j < 36; j++) {
+ if (!uuid[j]) {
+ r = 0 | (Math.random() * 16);
+ uuid[j] = chars[j === 19 ? (r & 0x3) | 0x8 : r];
}
}
+ }
- return uuid.join('');
+ return uuid.join('');
};
/**
@@ -43,28 +276,92 @@ Util.uuid = function(len,radix) {
*/
Util.mixin = function (base, obj) {
- for (var key in base) {
- obj[key] = (obj[key]) ? obj[key] : base[key];
+ for (const key in base) {
+ obj[key] = obj[key] ? obj[key] : base[key];
}
return obj;
};
/**
* Makes a secure request to an ofx server and posts an OFX payload
+ * Uses connection pooling with advanced timeout and retry logic, plus caching
+ * @param options - Request options including url, headers, operationType, etc.
+ * @param ofxPayload - OFX payload to send
+ * @param cb - Callback function (error, response)
+ */
+Util.request = function (options, ofxPayload, cb) {
+ // Check if caching should be used
+ const useCache = options.useCache !== false; // Default to true unless explicitly disabled
+ const cacheOperation = options.cacheOperation || 'statement';
+ const cacheParams = options.cacheParams || {};
+
+ // Check if connection pooling should be used
+ const usePooling = options.usePooling !== false; // Default to true unless explicitly disabled
+
+ debug('Making request with options:', {
+ url: options.url,
+ operationType: options.operationType,
+ usePooling: usePooling,
+ useCache: useCache,
+ cacheOperation: cacheOperation
+ });
+
+ // Try to get cached response first
+ if (useCache) {
+ const cacheManager = getCacheManager();
+ const cachedResponse = cacheManager.get(cacheOperation, cacheParams);
+
+ if (cachedResponse) {
+ debug('Returning cached response for operation:', cacheOperation);
+ return process.nextTick(() => cb(null, cachedResponse));
+ }
+ }
+
+ // Make the actual request
+ const performRequest = (err, response) => {
+ if (err) return cb(err, response);
+
+ // Cache the successful response
+ if (useCache && response) {
+ try {
+ const cacheManager = getCacheManager();
+ cacheManager.set(cacheOperation, cacheParams, response);
+ debug('Cached response for operation:', cacheOperation);
+ } catch (cacheError) {
+ debug('Cache set error (non-fatal):', cacheError.message);
+ // Cache errors are non-fatal, continue with the response
+ }
+ }
+
+ cb(err, response);
+ };
+
+ if (usePooling) {
+ // Use connection pooling for better performance with retry logic
+ const pool = getPool();
+ pool.request(options, ofxPayload, performRequest);
+ } else {
+ // Fall back to legacy TLS socket implementation
+ Util.requestLegacy(options, ofxPayload, performRequest);
+ }
+};
+
+/**
+ * Legacy TLS socket implementation (for backward compatibility)
* @param options
* @param ofxPayload
* @param cb
*/
-Util.request = function(options, ofxPayload, cb) {
- var parsedUrl = url.parse(options.url);
- var tlsOpts = {
- port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
+Util.requestLegacy = function (options, ofxPayload, cb) {
+ const parsedUrl = url.parse(options.url);
+ const tlsOpts = {
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? HTTPS_PORT : HTTP_PORT),
host: parsedUrl.hostname
};
- var socket = tls.connect(tlsOpts, function() {
- var buffer = 'POST ' + parsedUrl.path + ' HTTP/1.1\r\n';
- options.headers.forEach(function(header) {
- var value;
+ const socket = tls.connect(tlsOpts, () => {
+ let buffer = `POST ${parsedUrl.path} HTTP/1.1\r\n`;
+ options.headers.forEach(header => {
+ let value;
if (options[header]) {
value = options[header];
} else if (header === 'Content-Length') {
@@ -72,20 +369,20 @@ Util.request = function(options, ofxPayload, cb) {
} else if (header === 'Host') {
value = parsedUrl.host;
}
- buffer += header + ': ' + value + '\r\n';
+ buffer += `${header}: ${value}\r\n`;
});
buffer += '\r\n';
buffer += ofxPayload;
socket.write(buffer);
});
- var data = '';
- socket.on('data', function(chunk) {
+ let data = '';
+ socket.on('data', chunk => {
data += chunk;
});
- socket.on('end', function() {
- var error = true;
- var httpHeaderMatcher = new RegExp(/HTTP\/\d\.\d (\d{3}) (.*)/);
- var matches = httpHeaderMatcher.exec(data);
+ socket.on('end', () => {
+ let error = true;
+ const httpHeaderMatcher = new RegExp(/HTTP\/\d\.\d (\d{3}) (.*)/);
+ const matches = httpHeaderMatcher.exec(data);
if (matches && matches.length > 2) {
if (parseInt(matches[1], 10) === 200) {
error = false;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..dd8a47c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5108 @@
+{
+ "name": "banking",
+ "version": "1.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "banking",
+ "version": "1.2.0",
+ "dependencies": {
+ "debug": "^2.3.3",
+ "xml2js": "^0.6.2"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "^9.35.0",
+ "@stylistic/eslint-plugin": "^5.3.1",
+ "@types/node": "^24.3.3",
+ "@vitest/coverage-v8": "^3.2.4",
+ "@vitest/ui": "^3.2.4",
+ "eslint": "^9.35.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-n": "^17.21.3",
+ "eslint-plugin-promise": "^7.2.1",
+ "eslint-plugin-security": "^3.0.1",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.1.6",
+ "mocha": "^11.7.2",
+ "msw": "^2.11.2",
+ "nock": "^14.0.10",
+ "prettier": "^3.6.2",
+ "should": "^11.1.1",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@bundled-es-modules/cookie": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz",
+ "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cookie": "^0.7.2"
+ }
+ },
+ "node_modules/@bundled-es-modules/statuses": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz",
+ "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "statuses": "^2.0.1"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/debug": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/debug": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.35.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.16",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.16.tgz",
+ "integrity": "sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.2.0",
+ "@inquirer/type": "^3.0.8"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.0.tgz",
+ "integrity": "sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/figures": "^1.0.13",
+ "@inquirer/type": "^3.0.8",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
+ "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz",
+ "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.39.6",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.6.tgz",
+ "integrity": "sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz",
+ "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz",
+ "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz",
+ "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz",
+ "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz",
+ "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz",
+ "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz",
+ "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz",
+ "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz",
+ "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz",
+ "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz",
+ "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz",
+ "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz",
+ "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz",
+ "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz",
+ "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz",
+ "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz",
+ "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz",
+ "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz",
+ "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz",
+ "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz",
+ "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "5.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/types": "^8.41.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=9.0.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
+ "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.3.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz",
+ "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
+ },
+ "node_modules/@types/statuses": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
+ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.43.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
+ "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "ast-v8-to-istanbul": "^0.3.3",
+ "debug": "^4.4.1",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.9.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.2.4",
+ "vitest": "3.2.4"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/ui": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
+ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.3",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.1",
+ "tinyglobby": "^0.2.14",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "3.2.4"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz",
+ "integrity": "sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.30",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^9.0.1"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "14.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.35.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.1",
+ "@eslint/core": "^0.15.2",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.35.0",
+ "@eslint/plugin-kit": "^0.3.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-compat-utils": {
+ "version": "0.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "eslint": ">=6.0.0"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-es-x": {
+ "version": "7.8.0",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/ota-meshi",
+ "https://opencollective.com/eslint"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.1.2",
+ "@eslint-community/regexpp": "^4.11.0",
+ "eslint-compat-utils": "^0.5.1"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8"
+ }
+ },
+ "node_modules/eslint-plugin-n": {
+ "version": "17.21.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.5.0",
+ "enhanced-resolve": "^5.17.1",
+ "eslint-plugin-es-x": "^7.8.0",
+ "get-tsconfig": "^4.8.1",
+ "globals": "^15.11.0",
+ "globrex": "^0.1.2",
+ "ignore": "^5.3.2",
+ "semver": "^7.6.3",
+ "ts-declaration-location": "^1.0.6"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.23.0"
+ }
+ },
+ "node_modules/eslint-plugin-n/node_modules/globals": {
+ "version": "15.15.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "7.2.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-security": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-regex": "^2.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/debug": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+ "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.10.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphql": {
+ "version": "16.11.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/headers-polyfill": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lint-staged": {
+ "version": "16.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.6.0",
+ "commander": "^14.0.0",
+ "debug": "^4.4.1",
+ "lilconfig": "^3.1.3",
+ "listr2": "^9.0.3",
+ "micromatch": "^4.0.8",
+ "nano-spawn": "^1.0.2",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.2",
+ "yaml": "^2.8.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/chalk": {
+ "version": "5.6.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/debug": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lint-staged/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2": {
+ "version": "9.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "10.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "10.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.19",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "11.7.2",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz",
+ "integrity": "sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^4.0.1",
+ "debug": "^4.3.5",
+ "diff": "^7.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^10.4.5",
+ "he": "^1.2.0",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^9.0.5",
+ "ms": "^2.1.3",
+ "picocolors": "^1.1.1",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^9.2.0",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/mocha/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mocha/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "license": "MIT"
+ },
+ "node_modules/msw": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.2.tgz",
+ "integrity": "sha512-MI54hLCsrMwiflkcqlgYYNJJddY5/+S0SnONvhv1owOplvqohKSQyGejpNdUGyCwgs4IH7PqaNbPw/sKOEze9Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bundled-es-modules/cookie": "^2.0.1",
+ "@bundled-es-modules/statuses": "^1.0.1",
+ "@inquirer/confirm": "^5.0.0",
+ "@mswjs/interceptors": "^0.39.1",
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/until": "^2.1.0",
+ "@types/cookie": "^0.6.0",
+ "@types/statuses": "^2.0.4",
+ "graphql": "^16.8.1",
+ "headers-polyfill": "^4.0.2",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "path-to-regexp": "^6.3.0",
+ "picocolors": "^1.1.1",
+ "rettime": "^0.7.0",
+ "strict-event-emitter": "^0.5.1",
+ "tough-cookie": "^6.0.0",
+ "type-fest": "^4.26.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.8.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/nano-spawn": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nock": {
+ "version": "14.0.10",
+ "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz",
+ "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@mswjs/interceptors": "^0.39.5",
+ "json-stringify-safe": "^5.0.1",
+ "propagate": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18.20.0 <20 || >=20.12.1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/propagate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
+ "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/regexp-tree": {
+ "version": "0.1.27",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "regexp-tree": "bin/regexp-tree"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/rettime": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
+ "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.50.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz",
+ "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.50.1",
+ "@rollup/rollup-android-arm64": "4.50.1",
+ "@rollup/rollup-darwin-arm64": "4.50.1",
+ "@rollup/rollup-darwin-x64": "4.50.1",
+ "@rollup/rollup-freebsd-arm64": "4.50.1",
+ "@rollup/rollup-freebsd-x64": "4.50.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.50.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.50.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.50.1",
+ "@rollup/rollup-linux-arm64-musl": "4.50.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.50.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.50.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.50.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.50.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.50.1",
+ "@rollup/rollup-linux-x64-gnu": "4.50.1",
+ "@rollup/rollup-linux-x64-musl": "4.50.1",
+ "@rollup/rollup-openharmony-arm64": "4.50.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.50.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.50.1",
+ "@rollup/rollup-win32-x64-msvc": "4.50.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regexp-tree": "~0.1.1"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "license": "ISC"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/should": {
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/should/-/should-11.2.1.tgz",
+ "integrity": "sha512-TsX7f9e6f7h+ijEJwga7baYTCspzs0KJRJafafCqiPpgp2/eZ1b7LK3RNZZ5uIK8J0nhFOXVvpbcRwy09piO4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "should-equal": "^1.0.0",
+ "should-format": "^3.0.2",
+ "should-type": "^1.4.0",
+ "should-type-adaptors": "^1.0.1",
+ "should-util": "^1.0.0"
+ }
+ },
+ "node_modules/should-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz",
+ "integrity": "sha512-V78BKdBq92TbvEHjVJRwWItXtV6G813dMBviJZ01sn57sdOqb8pJUPHZhs9vUa8e6wQOHzPW7RTWv5wFFyJYmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "should-type": "^1.0.0"
+ }
+ },
+ "node_modules/should-format": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
+ "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "should-type": "^1.3.0",
+ "should-type-adaptors": "^1.0.1"
+ }
+ },
+ "node_modules/should-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+ "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/should-type-adaptors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
+ "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "should-type": "^1.3.0",
+ "should-util": "^1.0.0"
+ }
+ },
+ "node_modules/should-util": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz",
+ "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
+ "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
+ "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz",
+ "integrity": "sha512-lMNHE4aSI3LlkMUMicTmAG3tkkitjOQGDTFboPJwAg2kJXKP1ryWEyqujktg5qhrFZOkk5YFzgkxg3jErE+i5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.14"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.14.tgz",
+ "integrity": "sha512-viZGNK6+NdluOJWwTO9olaugx0bkKhscIdriQQ+lNNhwitIKvb+SvhbYgnCz6j9p7dX3cJntt4agQAKMXLjJ5g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/ts-declaration-location": {
+ "version": "1.0.7",
+ "dev": true,
+ "funding": [
+ {
+ "type": "ko-fi",
+ "url": "https://ko-fi.com/rebeccastevens"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/ts-declaration-location"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "picomatch": "^4.0.2"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.0.0"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
+ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz",
+ "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+ "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 5006346..0e16204 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,9 @@
"ofx",
"financial",
"bank",
- "quickbooks"
+ "quickbooks",
+ "typescript",
+ "types"
],
"private": false,
"homepage": "http://euforic.github.com/banking.js",
@@ -16,17 +18,54 @@
"debug": "^2.3.3",
"xml2js": "^0.6.2"
},
- "devDependencies": {
- "mocha": "^3.2.0",
- "should": "^11.1.1"
- },
"repository": {
"type": "git",
"url": "http://github.com/euforic/banking.js.git"
},
- "main": "lib/banking.js",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "engines": {
+ "node": ">=18.0.0"
+ },
"scripts": {
"prepublish": "npm prune",
- "test": "mocha --require should --reporter spec --globals i"
+ "test": "vitest run",
+ "test:watch": "vitest watch",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest run --coverage",
+ "test:integration": "vitest run test/integration/",
+ "test:legacy": "mocha --require should --reporter spec --globals i test/parsing.js",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "lint:check": "eslint . --max-warnings 200",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "format:diff": "prettier --check --list-different .",
+ "validate": "npm run format:check && npm run lint:check",
+ "fix": "npm run format && npm run lint:fix",
+ "pretest": "npm run validate",
+ "prepare": "husky"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "^9.35.0",
+ "@stylistic/eslint-plugin": "^5.3.1",
+ "@types/node": "^24.3.3",
+ "@vitest/coverage-v8": "^3.2.4",
+ "@vitest/ui": "^3.2.4",
+ "eslint": "^9.35.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-n": "^17.21.3",
+ "eslint-plugin-promise": "^7.2.1",
+ "eslint-plugin-security": "^3.0.1",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.1.6",
+ "mocha": "^11.7.2",
+ "msw": "^2.11.2",
+ "nock": "^14.0.10",
+ "prettier": "^3.6.2",
+ "should": "^11.1.1",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
}
}
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..f4de82c
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,306 @@
+# Banking.js Integration Test Suite
+
+This directory contains comprehensive integration tests for the Banking.js
+library, providing reliable testing coverage for OFX connectivity and parsing
+across multiple major financial institutions.
+
+## Overview
+
+The test suite includes:
+
+- **Integration tests** for 5 major banks (Wells Fargo, Discover, Chase, Bank of
+ America, US Bank)
+- **Comprehensive error handling** tests for network, OFX protocol, and parsing
+ errors
+- **Edge case testing** for boundary conditions, special characters, and unusual
+ scenarios
+- **Mock HTTP server** setup for reliable CI/CD testing
+- **Sandbox mode support** for testing against real bank environments
+
+## Quick Start
+
+```bash
+# Run all integration tests (mock mode)
+npm test
+
+# Run integration tests only
+npm run test:integration
+
+# Run with coverage report
+npm run test:coverage
+
+# Run in watch mode for development
+npm run test:watch
+
+# Open Vitest UI
+npm run test:ui
+
+# Run legacy Mocha tests
+npm run test:legacy
+```
+
+## Test Structure
+
+```
+test/
+āāā README.md # This file
+āāā setup.js # Global test setup and configuration
+āāā test-runner.js # Custom test runner with advanced options
+āāā fixtures/
+ā āāā data.js # Legacy test data
+ā āāā responses.js # Mock OFX responses for all banks
+ā āāā sample.ofx # Sample OFX files
+ā āāā sample-with-end-tags.ofx
+āāā integration/
+ā āāā wells-fargo.test.js # Wells Fargo integration tests
+ā āāā discover.test.js # Discover Financial tests
+ā āāā chase.test.js # Chase Bank tests
+ā āāā bank-of-america.test.js # Bank of America tests
+ā āāā us-bank.test.js # US Bank tests
+ā āāā error-handling.test.js # Comprehensive error scenarios
+ā āāā edge-cases.test.js # Edge cases and boundary testing
+āāā parsing.js # Legacy parsing tests (Mocha)
+```
+
+## Test Modes
+
+### Mock Mode (Default)
+
+Uses [nock](https://github.com/nock/nock) to intercept HTTP requests and return
+predefined responses. This mode:
+
+- ā
Runs completely offline
+- ā
Fast and reliable for CI/CD
+- ā
No real bank credentials required
+- ā
Tests OFX parsing and error handling
+
+### Sandbox Mode
+
+Attempts to connect to real bank sandbox environments (where available):
+
+- šļø Tests actual network connectivity
+- šļø Validates real OFX endpoints
+- ā ļø Requires sandbox credentials
+- ā ļø May be slower and less reliable
+
+```bash
+# Run in sandbox mode
+node test/test-runner.js --sandbox
+
+# Run specific bank in sandbox mode
+node test/test-runner.js --sandbox --bank wells-fargo
+```
+
+## Supported Banks
+
+Each bank has comprehensive test coverage including:
+
+| Bank | FID | Test Coverage | Sandbox Support |
+| ------------------ | ---- | ------------- | --------------- |
+| Wells Fargo | 3001 | ā
Full | š¶ Limited |
+| Discover Financial | 7101 | ā
Full | š¶ Limited |
+| Chase Bank | 636 | ā
Full | ā No |
+| Bank of America | 5959 | ā
Full | ā No |
+| US Bank | 1001 | ā
Full | š¶ Limited |
+
+### Test Coverage Per Bank
+
+- ā
Statement retrieval (`getStatement`)
+- ā
Account listing (`getAccounts`)
+- ā
Authentication error handling
+- ā
Network error scenarios
+- ā
OFX parsing validation
+- ā
Bank-specific features
+- ā
Edge cases and boundary conditions
+
+## Advanced Usage
+
+### Custom Test Runner
+
+The `test-runner.js` script provides advanced options:
+
+```bash
+# Show all available options
+node test/test-runner.js --help
+
+# Run specific bank with verbose output
+node test/test-runner.js --bank chase --verbose
+
+# Run with coverage and timeout adjustment
+node test/test-runner.js --coverage --timeout 60000
+
+# Serial execution for debugging
+node test/test-runner.js --serial --verbose
+```
+
+### Environment Variables
+
+Configure test behavior with environment variables:
+
+```bash
+# Enable debug logging
+DEBUG=banking:* npm test
+
+# Set custom timeout
+BANKING_REQUEST_TIMEOUT=30000 npm test
+
+# CI mode (affects output format)
+CI=true npm test
+
+# Sandbox mode with credentials
+BANKING_TEST_MODE=sandbox npm test
+```
+
+## Writing New Tests
+
+### Adding a New Bank
+
+1. **Create test file**: `test/integration/new-bank.test.js`
+2. **Add bank config**: Update `bankConfigs` in `fixtures/responses.js`
+3. **Create mock responses**: Add OFX response fixtures
+4. **Update test runner**: Add bank name to `supportedBanks` array
+
+### Test Structure Template
+
+```javascript
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { bankConfigs } from '../fixtures/responses.js';
+
+describe('New Bank Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.newBank);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve statement', done => {
+ nock('https://bank-ofx-endpoint.com')
+ .post('/ofx')
+ .reply(200, mockResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ // Add specific assertions
+ done();
+ });
+ });
+ });
+});
+```
+
+## Best Practices
+
+### Test Isolation
+
+- Each test cleans up HTTP mocks automatically
+- No shared state between tests
+- Deterministic test execution
+
+### Error Handling
+
+- Test both success and failure scenarios
+- Verify specific error codes and messages
+- Test network timeouts and retries
+
+### Mock Responses
+
+- Use realistic OFX data structures
+- Include edge cases in fixtures
+- Maintain consistency with real bank responses
+
+### Performance
+
+- Tests run in parallel by default
+- Use `--serial` flag for debugging
+- Timeout configuration for slow connections
+
+## Troubleshooting
+
+### Common Issues
+
+**Tests failing with connection errors:**
+
+```bash
+# Check if nock is properly mocking requests
+DEBUG=nock* npm test
+```
+
+**Timeout errors:**
+
+```bash
+# Increase timeout
+node test/test-runner.js --timeout 60000
+```
+
+**Parsing errors:**
+
+```bash
+# Run with verbose output
+node test/test-runner.js --verbose
+```
+
+### Debugging
+
+1. **Enable debug logging:**
+
+ ```bash
+ DEBUG=banking:* npm test
+ ```
+
+2. **Run single test file:**
+
+ ```bash
+ npx vitest run test/integration/wells-fargo.test.js
+ ```
+
+3. **Use Vitest UI for interactive debugging:**
+ ```bash
+ npm run test:ui
+ ```
+
+## CI/CD Integration
+
+### GitHub Actions Example
+
+```yaml
+- name: Run Integration Tests
+ run: |
+ npm run test:coverage
+ env:
+ CI: true
+ NODE_ENV: test
+```
+
+### Test Reports
+
+Coverage reports are generated in:
+
+- `coverage/` directory (HTML)
+- Console output (text)
+- JSON format for CI tools
+
+## Contributing
+
+When contributing new tests:
+
+1. Follow existing patterns and structure
+2. Add both positive and negative test cases
+3. Include comprehensive error handling
+4. Update this README if adding new features
+5. Ensure all tests pass in both mock and sandbox modes
+
+## Security Notes
+
+- **Never commit real bank credentials**
+- Use environment variables for sandbox testing
+- Mock sensitive data in fixtures
+- Follow responsible disclosure for security issues
+
+---
+
+For more information about the Banking.js library, see the main
+[README.md](../README.md).
diff --git a/test/fixtures/data.js b/test/fixtures/data.js
index c824cae..0180487 100644
--- a/test/fixtures/data.js
+++ b/test/fixtures/data.js
@@ -1 +1,3 @@
-exports.ofxString = unescape('OFXHEADER%3A100%0ADATA%3AOFXSGML%0AVERSION%3A102%0ASECURITY%3ANONE%0AENCODING%3AUSASCII%0ACHARSET%3A1252%0ACOMPRESSION%3ANONE%0AOLDFILEUID%3ANONE%0ANEWFILEUID%3ANONE%0A%0A%3COFX%3E%0A%20%20%3CSIGNONMSGSRSV1%3E%0A%20%20%20%20%3CSONRS%3E%0A%20%20%20%20%20%20%3CSTATUS%3E%0A%20%20%20%20%20%20%20%20%3CCODE%3E0%0A%20%20%20%20%20%20%20%20%3CSEVERITY%3EINFO%0A%20%20%20%20%20%20%3C/STATUS%3E%0A%20%20%20%20%20%20%3CDTSERVER%3E20120127235919.500%0A%20%20%20%20%20%20%3CLANGUAGE%3EENG%0A%20%20%20%20%20%20%3CDTPROFUP%3E20050531070000.000%0A%20%20%20%20%20%20%3CFI%3E%0A%20%20%20%20%20%20%20%20%3CORG%3EWFB%0A%20%20%20%20%20%20%20%20%3CFID%3E3000%0A%20%20%20%20%20%20%3C/FI%3E%0A%20%20%20%20%20%20%3CINTU.BID%3E3000%0A%20%20%20%20%20%20%3CINTU.USERID%3Exxx34tf%0A%20%20%20%20%3C/SONRS%3E%0A%20%20%3C/SIGNONMSGSRSV1%3E%0A%20%20%3CBANKMSGSRSV1%3E%0A%20%20%20%20%3CSTMTTRNRS%3E%0A%20%20%20%20%20%20%3CTRNUID%3E0%0A%20%20%20%20%20%20%3CSTATUS%3E%0A%20%20%20%20%20%20%20%20%3CCODE%3E0%0A%20%20%20%20%20%20%20%20%3CSEVERITY%3EINFO%0A%20%20%20%20%20%20%3C/STATUS%3E%0A%20%20%20%20%20%20%3CSTMTRS%3E%0A%20%20%20%20%20%20%20%20%3CCURDEF%3EUSD%0A%20%20%20%20%20%20%20%20%3CBANKACCTFROM%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBANKID%3E000000000%0A%20%20%20%20%20%20%20%20%20%20%3CACCTID%3E1234567890%0A%20%20%20%20%20%20%20%20%20%20%3CACCTTYPE%3ECHECKING%0A%20%20%20%20%20%20%20%20%3C/BANKACCTFROM%3E%0A%20%20%20%20%20%20%20%20%3CBANKTRANLIST%3E%0A%20%20%20%20%20%20%20%20%20%20%3CDTSTART%3E20120101080000.000%0A%20%20%20%20%20%20%20%20%20%20%3CDTEND%3E20120126080000.000%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120103120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-49.95%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201031%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EPLANET%20BEACH%20AL001%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3ERECUR%20DEBIT%20CRD%20PMT0%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-39.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201053%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-028490%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E916.01%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201054%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-3408.83%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201051%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-002394%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-48.36%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201052%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-002393%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120109120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-114.70%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201091%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EATT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EPayment%20JUAN%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120109120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E114.70%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201092%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120112120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E180.69%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201122%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120112120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-180.69%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201121%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EVERIZON%20WIRELESS%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EPAYMENTS%20120112%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EFEE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120125120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-9.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201251%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EMONTHLY%20SERVICE%20FEE%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120126120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E25.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201261%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%3C/BANKTRANLIST%3E%0A%20%20%20%20%20%20%20%20%3CLEDGERBAL%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBALAMT%3E16.00%0A%20%20%20%20%20%20%20%20%20%20%3CDTASOF%3E20120126080000.000%5B0%3AGMT%5D%0A%20%20%20%20%20%20%20%20%3C/LEDGERBAL%3E%0A%20%20%20%20%20%20%20%20%3CAVAILBAL%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBALAMT%3E16.00%0A%20%20%20%20%20%20%20%20%20%20%3CDTASOF%3E20120126080000.000%5B0%3AGMT%5D%0A%20%20%20%20%20%20%20%20%3C/AVAILBAL%3E%0A%20%20%20%20%20%20%3C/STMTRS%3E%0A%20%20%20%20%3C/STMTTRNRS%3E%0A%20%20%3C/BANKMSGSRSV1%3E%0A%3C/OFX%3E');
\ No newline at end of file
+exports.ofxString = unescape(
+ 'OFXHEADER%3A100%0ADATA%3AOFXSGML%0AVERSION%3A102%0ASECURITY%3ANONE%0AENCODING%3AUSASCII%0ACHARSET%3A1252%0ACOMPRESSION%3ANONE%0AOLDFILEUID%3ANONE%0ANEWFILEUID%3ANONE%0A%0A%3COFX%3E%0A%20%20%3CSIGNONMSGSRSV1%3E%0A%20%20%20%20%3CSONRS%3E%0A%20%20%20%20%20%20%3CSTATUS%3E%0A%20%20%20%20%20%20%20%20%3CCODE%3E0%0A%20%20%20%20%20%20%20%20%3CSEVERITY%3EINFO%0A%20%20%20%20%20%20%3C/STATUS%3E%0A%20%20%20%20%20%20%3CDTSERVER%3E20120127235919.500%0A%20%20%20%20%20%20%3CLANGUAGE%3EENG%0A%20%20%20%20%20%20%3CDTPROFUP%3E20050531070000.000%0A%20%20%20%20%20%20%3CFI%3E%0A%20%20%20%20%20%20%20%20%3CORG%3EWFB%0A%20%20%20%20%20%20%20%20%3CFID%3E3000%0A%20%20%20%20%20%20%3C/FI%3E%0A%20%20%20%20%20%20%3CINTU.BID%3E3000%0A%20%20%20%20%20%20%3CINTU.USERID%3Exxx34tf%0A%20%20%20%20%3C/SONRS%3E%0A%20%20%3C/SIGNONMSGSRSV1%3E%0A%20%20%3CBANKMSGSRSV1%3E%0A%20%20%20%20%3CSTMTTRNRS%3E%0A%20%20%20%20%20%20%3CTRNUID%3E0%0A%20%20%20%20%20%20%3CSTATUS%3E%0A%20%20%20%20%20%20%20%20%3CCODE%3E0%0A%20%20%20%20%20%20%20%20%3CSEVERITY%3EINFO%0A%20%20%20%20%20%20%3C/STATUS%3E%0A%20%20%20%20%20%20%3CSTMTRS%3E%0A%20%20%20%20%20%20%20%20%3CCURDEF%3EUSD%0A%20%20%20%20%20%20%20%20%3CBANKACCTFROM%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBANKID%3E000000000%0A%20%20%20%20%20%20%20%20%20%20%3CACCTID%3E1234567890%0A%20%20%20%20%20%20%20%20%20%20%3CACCTTYPE%3ECHECKING%0A%20%20%20%20%20%20%20%20%3C/BANKACCTFROM%3E%0A%20%20%20%20%20%20%20%20%3CBANKTRANLIST%3E%0A%20%20%20%20%20%20%20%20%20%20%3CDTSTART%3E20120101080000.000%0A%20%20%20%20%20%20%20%20%20%20%3CDTEND%3E20120126080000.000%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120103120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-49.95%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201031%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EPLANET%20BEACH%20AL001%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3ERECUR%20DEBIT%20CRD%20PMT0%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-39.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201053%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-028490%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E916.01%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201054%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-3408.83%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201051%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-002394%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120105120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-48.36%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201052%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3ECITY%20OF%20FAIRHOPE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EUTILITIES%2024-002393%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120109120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-114.70%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201091%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EATT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EPayment%20JUAN%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120109120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E114.70%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201092%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120112120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E180.69%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201122%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EDIRECTDEBIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120112120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-180.69%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201121%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EVERIZON%20WIRELESS%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EPAYMENTS%20120112%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3EFEE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120125120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E-9.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201251%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EMONTHLY%20SERVICE%20FEE%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%3CSTMTTRN%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNTYPE%3ECREDIT%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CDTPOSTED%3E20120126120000.000%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CTRNAMT%3E25.00%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CFITID%3E201201261%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNAME%3EFROM%20CREDIT%20CARD%20OR%20LINE%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMEMO%3EOVERDRAFT%20XFER%0A%20%20%20%20%20%20%20%20%20%20%3C/STMTTRN%3E%0A%20%20%20%20%20%20%20%20%3C/BANKTRANLIST%3E%0A%20%20%20%20%20%20%20%20%3CLEDGERBAL%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBALAMT%3E16.00%0A%20%20%20%20%20%20%20%20%20%20%3CDTASOF%3E20120126080000.000%5B0%3AGMT%5D%0A%20%20%20%20%20%20%20%20%3C/LEDGERBAL%3E%0A%20%20%20%20%20%20%20%20%3CAVAILBAL%3E%0A%20%20%20%20%20%20%20%20%20%20%3CBALAMT%3E16.00%0A%20%20%20%20%20%20%20%20%20%20%3CDTASOF%3E20120126080000.000%5B0%3AGMT%5D%0A%20%20%20%20%20%20%20%20%3C/AVAILBAL%3E%0A%20%20%20%20%20%20%3C/STMTRS%3E%0A%20%20%20%20%3C/STMTTRNRS%3E%0A%20%20%3C/BANKMSGSRSV1%3E%0A%3C/OFX%3E'
+);
diff --git a/test/fixtures/responses.js b/test/fixtures/responses.js
new file mode 100644
index 0000000..9deca7e
--- /dev/null
+++ b/test/fixtures/responses.js
@@ -0,0 +1,585 @@
+// Mock OFX responses for different banks
+// These responses are based on actual OFX format but with dummy data
+
+export const wellsFargoStatementResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-wells-fargo
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-8:PST]
+ ENG
+
+ Wells Fargo
+ 3001
+
+
+
+
+
+ test-transaction-uid-123
+
+ 0
+ INFO
+ SUCCESS
+
+
+ USD
+
+ 123006800
+ 1234567890
+ CHECKING
+
+
+ 20241101120000.000
+ 20241201120000.000
+
+ DEBIT
+ 20241115120000.000
+ -150.00
+ WF202411150001
+ GROCERY STORE PURCHASE
+ WHOLE FOODS MARKET
+
+
+ CREDIT
+ 20241116120000.000
+ 2500.00
+ WF202411160001
+ DIRECT DEPOSIT
+ PAYROLL DEPOSIT
+
+
+ FEE
+ 20241120120000.000
+ -12.00
+ WF202411200001
+ MONTHLY SERVICE FEE
+ ACCOUNT MAINTENANCE FEE
+
+
+
+ 2338.00
+ 20241201120000.000[-8:PST]
+
+
+ 2338.00
+ 20241201120000.000[-8:PST]
+
+
+
+
+ `;
+
+export const wellsFargoAccountListResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-wells-fargo-accounts
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-8:PST]
+ ENG
+
+ Wells Fargo
+ 3001
+
+
+
+
+
+ account-list-uid-123
+
+ 0
+ INFO
+ SUCCESS
+
+
+ 20241201120000.000
+
+
+
+ 123006800
+ 1234567890
+ CHECKING
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 123006800
+ 9876543210
+ SAVINGS
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ `;
+
+export const discoverCardStatementResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-discover-card
+
+
+
+
+
+ 0
+ INFO
+
+ 20241201120000.000[-5:EST]
+ ENG
+
+ Discover Financial Services
+ 7101
+
+
+
+
+
+ discover-transaction-uid-456
+
+ 0
+ INFO
+
+
+ USD
+
+ 6011123456789012
+
+
+ 20241101120000.000
+ 20241201120000.000
+
+ DEBIT
+ 20241115120000.000
+ -89.99
+ DISC202411150001
+ AMAZON.COM
+ ONLINE PURCHASE
+
+
+ DEBIT
+ 20241118120000.000
+ -45.67
+ DISC202411180001
+ SHELL GAS STATION
+ FUEL PURCHASE
+
+
+ CREDIT
+ 20241125120000.000
+ 100.00
+ DISC202411250001
+ PAYMENT THANK YOU
+ ONLINE PAYMENT
+
+
+
+ -35.66
+ 20241201120000.000[-5:EST]
+
+
+
+
+ `;
+
+export const chaseStatementResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-chase-bank
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-5:EST]
+ ENG
+
+ JPMorgan Chase Bank, N.A.
+ 636
+
+ 636
+
+
+
+
+ chase-transaction-uid-789
+
+ 0
+ INFO
+ SUCCESS
+
+
+ USD
+
+ 322271627
+ 5555666677
+ CHECKING
+
+
+ 20241101120000.000
+ 20241201120000.000
+
+ DEBIT
+ 20241110120000.000
+ -75.00
+ CHASE202411100001
+ ATM WITHDRAWAL
+ CHASE ATM #1234
+
+
+ CREDIT
+ 20241115120000.000
+ 3000.00
+ CHASE202411150001
+ DIRECT DEPOSIT
+ EMPLOYER PAYROLL
+
+
+ DEBIT
+ 20241120120000.000
+ -1200.00
+ CHASE202411200001
+ RENT PAYMENT
+ AUTO PAY RENT
+
+
+
+ 1725.00
+ 20241201120000.000[-5:EST]
+
+
+ 1725.00
+ 20241201120000.000[-5:EST]
+
+
+
+
+ `;
+
+export const bankOfAmericaStatementResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-boa
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-8:PST]
+ ENG
+
+ Bank of America
+ 5959
+
+
+
+
+
+ boa-transaction-uid-101
+
+ 0
+ INFO
+ SUCCESS
+
+
+ USD
+
+ 026009593
+ 3333444455
+ CHECKING
+
+
+ 20241101120000.000
+ 20241201120000.000
+
+ DEBIT
+ 20241112120000.000
+ -125.50
+ BOA202411120001
+ COSTCO WHOLESALE
+ DEBIT CARD PURCHASE
+
+
+ CREDIT
+ 20241115120000.000
+ 2800.00
+ BOA202411150001
+ DIRECT DEPOSIT
+ SALARY DEPOSIT
+
+
+ FEE
+ 20241130120000.000
+ -25.00
+ BOA202411300001
+ OVERDRAFT FEE
+ INSUFFICIENT FUNDS FEE
+
+
+
+ 2649.50
+ 20241201120000.000[-8:PST]
+
+
+ 2649.50
+ 20241201120000.000[-8:PST]
+
+
+
+
+ `;
+
+export const usBankStatementResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-usbank
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-6:CST]
+ ENG
+
+ U.S. Bank
+ 1001
+
+
+
+
+
+ usbank-transaction-uid-202
+
+ 0
+ INFO
+ SUCCESS
+
+
+ USD
+
+ 091000022
+ 7777888899
+ SAVINGS
+
+
+ 20241101120000.000
+ 20241201120000.000
+
+ CREDIT
+ 20241105120000.000
+ 5000.00
+ USB202411050001
+ TRANSFER FROM CHECKING
+ INTERNAL TRANSFER
+
+
+ CREDIT
+ 20241115120000.000
+ 15.25
+ USB202411150001
+ INTEREST PAYMENT
+ MONTHLY INTEREST
+
+
+
+ 15015.25
+ 20241201120000.000[-6:CST]
+
+
+ 15015.25
+ 20241201120000.000[-6:CST]
+
+
+
+
+ `;
+
+// Error responses for testing error handling
+export const invalidCredentialsResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 15500
+ ERROR
+ INVALID SIGNON
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+export const accountNotFoundResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ error-transaction-uid
+
+ 10500
+ ERROR
+ INVALID ACCOUNT NUMBER
+
+
+
+ `;
+
+export const malformedOFXResponse = `INVALID OFX DATA
+THIS IS NOT PROPER OFX FORMAT
+
+ WITHOUT HEADER
+ MISSING CLOSING TAGS
+ `;
+
+// Bank configuration data for tests
+export const bankConfigs = {
+ wellsFargo: {
+ fid: 3001,
+ fidOrg: 'Wells Fargo',
+ url: 'https://www.oasis.cfree.com/3001.ofxgp',
+ bankId: '123006800',
+ accType: 'CHECKING',
+ accId: '1234567890',
+ user: 'testuser',
+ password: 'testpass'
+ },
+ discover: {
+ fid: 7101,
+ fidOrg: 'Discover Financial Services',
+ url: 'https://ofx.discovercard.com',
+ bankId: '',
+ accType: 'CREDITCARD',
+ accId: '6011123456789012',
+ user: 'testuser',
+ password: 'testpass'
+ },
+ chase: {
+ fid: 636,
+ fidOrg: 'JPMorgan Chase Bank, N.A.',
+ url: 'https://ofx.chase.com',
+ bankId: '322271627',
+ accType: 'CHECKING',
+ accId: '5555666677',
+ user: 'testuser',
+ password: 'testpass',
+ clientId: 'test-client-id-123'
+ },
+ bankOfAmerica: {
+ fid: 5959,
+ fidOrg: 'Bank of America',
+ url: 'https://eftx.bankofamerica.com/eftxweb/access.ofx',
+ bankId: '026009593',
+ accType: 'CHECKING',
+ accId: '3333444455',
+ user: 'testuser',
+ password: 'testpass'
+ },
+ usBank: {
+ fid: 1001,
+ fidOrg: 'U.S. Bank',
+ url: 'https://www.usbank.com/ofxroot',
+ bankId: '091000022',
+ accType: 'SAVINGS',
+ accId: '7777888899',
+ user: 'testuser',
+ password: 'testpass'
+ }
+};
diff --git a/test/integration/bank-of-america.test.js b/test/integration/bank-of-america.test.js
new file mode 100644
index 0000000..6b6fef0
--- /dev/null
+++ b/test/integration/bank-of-america.test.js
@@ -0,0 +1,489 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import {
+ bankOfAmericaStatementResponse,
+ invalidCredentialsResponse,
+ accountNotFoundResponse,
+ malformedOFXResponse,
+ bankConfigs
+} from '../fixtures/responses.js';
+
+describe('Bank of America Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.bankOfAmerica);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve Bank of America statement', done => {
+ // Mock the OFX server response
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, bankOfAmericaStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.ORG).toBe('Bank of America');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.FID).toBe('5959');
+
+ // Verify bank statement data
+ expect(ofx.BANKMSGSRSV1).toBeDefined();
+ const stmtrs = ofx.BANKMSGSRSV1.STMTTRNRS.STMTRS;
+ expect(stmtrs.CURDEF).toBe('USD');
+ expect(stmtrs.BANKACCTFROM.BANKID).toBe('026009593');
+ expect(stmtrs.BANKACCTFROM.ACCTID).toBe('3333444455');
+ expect(stmtrs.BANKACCTFROM.ACCTTYPE).toBe('CHECKING');
+
+ // Verify transactions
+ const transactions = stmtrs.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBeGreaterThan(0);
+
+ // Verify specific Bank of America transactions
+ const costcoTransaction = transactions.find(t => t.NAME === 'COSTCO WHOLESALE');
+ expect(costcoTransaction).toBeDefined();
+ expect(costcoTransaction.TRNTYPE).toBe('DEBIT');
+ expect(costcoTransaction.TRNAMT).toBe('-125.50');
+ expect(costcoTransaction.MEMO).toBe('DEBIT CARD PURCHASE');
+
+ const salaryTransaction = transactions.find(t => t.NAME === 'DIRECT DEPOSIT');
+ expect(salaryTransaction).toBeDefined();
+ expect(salaryTransaction.TRNTYPE).toBe('CREDIT');
+ expect(salaryTransaction.TRNAMT).toBe('2800.00');
+ expect(salaryTransaction.MEMO).toBe('SALARY DEPOSIT');
+
+ const feeTransaction = transactions.find(t => t.NAME === 'OVERDRAFT FEE');
+ expect(feeTransaction).toBeDefined();
+ expect(feeTransaction.TRNTYPE).toBe('FEE');
+ expect(feeTransaction.TRNAMT).toBe('-25.00');
+ expect(feeTransaction.MEMO).toBe('INSUFFICIENT FUNDS FEE');
+
+ // Verify balance information
+ expect(stmtrs.LEDGERBAL).toBeDefined();
+ expect(stmtrs.LEDGERBAL.BALAMT).toBe('2649.50');
+ expect(stmtrs.AVAILBAL).toBeDefined();
+ expect(stmtrs.AVAILBAL.BALAMT).toBe('2649.50');
+
+ done();
+ });
+ });
+
+ it('should handle Bank of America authentication errors', done => {
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('INVALID SIGNON');
+ done();
+ });
+ });
+
+ it('should send properly formatted Bank of America OFX request', done => {
+ const scope = nock('https://eftx.bankofamerica.com')
+ .post('/eftxweb/access.ofx', body => {
+ // Verify Bank of America-specific OFX request format
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('DATA:OFXSGML');
+ expect(body).toContain('VERSION:102');
+ expect(body).toContain('testuser');
+ expect(body).toContain('testpass');
+ expect(body).toContain('5959');
+ expect(body).toContain('026009593');
+ expect(body).toContain('3333444455');
+ expect(body).toContain('CHECKING');
+ expect(body).toContain('20241101');
+ expect(body).toContain('20241201');
+ return true;
+ })
+ .reply(200, bankOfAmericaStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle Bank of America server timeouts', done => {
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').delay(5000).reply(200, bankOfAmericaStatementResponse);
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(elapsedTime).toBeGreaterThan(1000);
+ done();
+ });
+ });
+
+ it('should handle Bank of America SSL certificate issues', done => {
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').replyWithError('UNABLE_TO_VERIFY_LEAF_SIGNATURE');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle Bank of America account restrictions', done => {
+ const restrictionResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ boa-restriction-error
+
+ 10401
+ ERROR
+ ACCOUNT RESTRICTED
+
+
+
+ `;
+
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, restrictionResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10401');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('ACCOUNT RESTRICTED');
+ done();
+ });
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should successfully retrieve Bank of America account list', done => {
+ const boaAccountListResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-boa-accounts
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-8:PST]
+ ENG
+
+ Bank of America
+ 5959
+
+
+
+
+
+ boa-accounts-uid-101
+
+ 0
+ INFO
+ SUCCESS
+
+
+ 20241201120000.000
+
+
+
+ 026009593
+ 3333444455
+ CHECKING
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 026009593
+ 3333444466
+ SAVINGS
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 4111111111111112
+
+ Y
+ N
+ N
+ ACTIVE
+
+
+
+
+
+ `;
+
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, boaAccountListResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ const acctInfo = res.body.OFX.SIGNUPMSGSRSV1.ACCTINFOTRNRS.ACCTINFORS.ACCTINFO;
+ expect(Array.isArray(acctInfo)).toBe(true);
+ expect(acctInfo.length).toBe(3); // Checking, savings, and credit card
+
+ // Verify checking account
+ const checkingAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'CHECKING');
+ expect(checkingAccount).toBeDefined();
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('3333444455');
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.BANKID).toBe('026009593');
+ expect(checkingAccount.BANKACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ // Verify savings account
+ const savingsAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'SAVINGS');
+ expect(savingsAccount).toBeDefined();
+ expect(savingsAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('3333444466');
+
+ // Verify credit card account
+ const creditCardAccount = acctInfo.find(acc => acc.CCACCTINFO);
+ expect(creditCardAccount).toBeDefined();
+ expect(creditCardAccount.CCACCTINFO.CCACCTFROM.ACCTID).toBe('4111111111111112');
+ expect(creditCardAccount.CCACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ done();
+ });
+ });
+ });
+
+ describe('Bank of America Specific Features', () => {
+ it('should handle Bank of America fee transactions correctly', done => {
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, bankOfAmericaStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+
+ // Verify fee transaction handling
+ const feeTransactions = transactions.filter(t => t.TRNTYPE === 'FEE');
+ expect(feeTransactions.length).toBeGreaterThan(0);
+
+ const overdraftFee = feeTransactions.find(t => t.NAME === 'OVERDRAFT FEE');
+ expect(overdraftFee).toBeDefined();
+ expect(parseFloat(overdraftFee.TRNAMT)).toBeLessThan(0);
+ expect(overdraftFee.MEMO).toBe('INSUFFICIENT FUNDS FEE');
+
+ done();
+ });
+ });
+
+ it('should handle Bank of America routing number validation', done => {
+ // Test with invalid routing number
+ const invalidBankId = new Banking({
+ ...bankConfigs.bankOfAmerica,
+ bankId: '000000000' // Invalid routing number
+ });
+
+ const invalidAccountResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ invalid-routing-error
+
+ 10400
+ ERROR
+ INVALID BANK ID
+
+
+
+ `;
+
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, invalidAccountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ invalidBankId.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10400');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID BANK ID');
+ done();
+ });
+ });
+
+ it('should handle Bank of America transaction limits', done => {
+ const limitResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ limit-error-transaction
+
+ 13000
+ ERROR
+ REQUEST TOO LARGE
+
+
+
+ `;
+
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, limitResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ // Test with very large date range (5 years)
+ banking.getStatement({ start: 20200101, end: 20241231 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('13000');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('REQUEST TOO LARGE');
+ done();
+ });
+ });
+
+ it('should handle different account types properly', done => {
+ // Test with a MONEYMRKT (Money Market) account
+ const mmBanking = new Banking({
+ ...bankConfigs.bankOfAmerica,
+ accType: 'MONEYMRKT',
+ accId: '7777888899'
+ });
+
+ const mmResponse = bankOfAmericaStatementResponse
+ .replace('CHECKING', 'MONEYMRKT')
+ .replace('3333444455', '7777888899');
+
+ const scope = nock('https://eftx.bankofamerica.com')
+ .post('/eftxweb/access.ofx', body => {
+ expect(body).toContain('MONEYMRKT');
+ expect(body).toContain('7777888899');
+ return true;
+ })
+ .reply(200, mmResponse);
+
+ mmBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKACCTFROM.ACCTTYPE).toBe('MONEYMRKT');
+ done();
+ });
+ });
+ });
+
+ describe('Error Recovery and Resilience', () => {
+ it('should handle partial response corruption', done => {
+ const partiallyCorruptedResponse = bankOfAmericaStatementResponse.replace(' ', 'CORRUPT DATA ');
+
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').reply(200, partiallyCorruptedResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ // Should still parse the valid parts
+ if (!err) {
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ }
+ done();
+ });
+ });
+
+ it('should handle network interruptions gracefully', done => {
+ nock('https://eftx.bankofamerica.com').post('/eftxweb/access.ofx').replyWithError('ECONNRESET');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ECONNRESET');
+ done();
+ });
+ });
+ });
+});
diff --git a/test/integration/chase.test.js b/test/integration/chase.test.js
new file mode 100644
index 0000000..df68fb1
--- /dev/null
+++ b/test/integration/chase.test.js
@@ -0,0 +1,520 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import {
+ chaseStatementResponse,
+ invalidCredentialsResponse,
+ accountNotFoundResponse,
+ malformedOFXResponse,
+ bankConfigs
+} from '../fixtures/responses.js';
+
+describe('Chase Bank Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.chase);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve Chase bank statement', done => {
+ // Mock the OFX server response
+ nock('https://ofx.chase.com').post('/').reply(200, chaseStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.ORG).toBe('JPMorgan Chase Bank, N.A.');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.FID).toBe('636');
+
+ // Verify Chase-specific INTU.BID field
+ expect(ofx.SIGNONMSGSRSV1.SONRS['INTU.BID']).toBe('636');
+
+ // Verify bank statement data
+ expect(ofx.BANKMSGSRSV1).toBeDefined();
+ const stmtrs = ofx.BANKMSGSRSV1.STMTTRNRS.STMTRS;
+ expect(stmtrs.CURDEF).toBe('USD');
+ expect(stmtrs.BANKACCTFROM.BANKID).toBe('322271627');
+ expect(stmtrs.BANKACCTFROM.ACCTID).toBe('5555666677');
+ expect(stmtrs.BANKACCTFROM.ACCTTYPE).toBe('CHECKING');
+
+ // Verify transactions
+ const transactions = stmtrs.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBeGreaterThan(0);
+
+ // Verify specific Chase transactions
+ const atmTransaction = transactions.find(t => t.NAME === 'ATM WITHDRAWAL');
+ expect(atmTransaction).toBeDefined();
+ expect(atmTransaction.TRNTYPE).toBe('DEBIT');
+ expect(atmTransaction.TRNAMT).toBe('-75.00');
+ expect(atmTransaction.MEMO).toBe('CHASE ATM #1234');
+
+ const payrollTransaction = transactions.find(t => t.NAME === 'DIRECT DEPOSIT');
+ expect(payrollTransaction).toBeDefined();
+ expect(payrollTransaction.TRNTYPE).toBe('CREDIT');
+ expect(payrollTransaction.TRNAMT).toBe('3000.00');
+
+ const rentTransaction = transactions.find(t => t.NAME === 'RENT PAYMENT');
+ expect(rentTransaction).toBeDefined();
+ expect(rentTransaction.TRNTYPE).toBe('DEBIT');
+ expect(rentTransaction.TRNAMT).toBe('-1200.00');
+ expect(rentTransaction.MEMO).toBe('AUTO PAY RENT');
+
+ // Verify balance information
+ expect(stmtrs.LEDGERBAL).toBeDefined();
+ expect(stmtrs.LEDGERBAL.BALAMT).toBe('1725.00');
+ expect(stmtrs.AVAILBAL).toBeDefined();
+ expect(stmtrs.AVAILBAL.BALAMT).toBe('1725.00');
+
+ done();
+ });
+ });
+
+ it('should handle Chase multi-factor authentication requirements', done => {
+ // Chase may require additional authentication steps
+ const mfaResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-chase-mfa
+
+
+
+
+
+ 15000
+ ERROR
+ MFA CHALLENGE REQUIRED
+
+ 20241201120000.000[-5:EST]
+ ENG
+
+ JPMorgan Chase Bank, N.A.
+ 636
+
+
+ Please check your email for verification code
+
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, mfaResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15000');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('MFA CHALLENGE REQUIRED');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.MFACHALLENGERQ).toBeDefined();
+ done();
+ });
+ });
+
+ it('should send Chase-specific request format with ClientUID', done => {
+ const scope = nock('https://ofx.chase.com')
+ .post('/', body => {
+ // Verify Chase-specific OFX request format
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('VERSION:103'); // Chase requires version 103
+ expect(body).toContain('testuser');
+ expect(body).toContain('testpass');
+ expect(body).toContain('636');
+ expect(body).toContain('322271627');
+ expect(body).toContain('5555666677');
+ expect(body).toContain('CHECKING');
+
+ // Chase-specific fields
+ if (bankConfigs.chase.clientId) {
+ expect(body).toContain('test-client-id-123');
+ }
+
+ return true;
+ })
+ .reply(200, chaseStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle Chase server errors gracefully', done => {
+ nock('https://ofx.chase.com').post('/').reply(503, 'Service Temporarily Unavailable');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle Chase account verification errors', done => {
+ const chaseAccountError = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ chase-error-transaction
+
+ 10403
+ ERROR
+ ACCOUNT VERIFICATION REQUIRED
+
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, chaseAccountError, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10403');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('ACCOUNT VERIFICATION REQUIRED');
+ done();
+ });
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should successfully retrieve Chase account list', done => {
+ const chaseAccountListResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-chase-accounts
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-5:EST]
+ ENG
+
+ JPMorgan Chase Bank, N.A.
+ 636
+
+ 636
+
+
+
+
+ chase-accounts-uid-789
+
+ 0
+ INFO
+ SUCCESS
+
+
+ 20241201120000.000
+
+
+
+ 322271627
+ 5555666677
+ CHECKING
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 322271627
+ 5555666688
+ SAVINGS
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 4111111111111111
+
+ Y
+ N
+ N
+ ACTIVE
+
+
+
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, chaseAccountListResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ const acctInfo = res.body.OFX.SIGNUPMSGSRSV1.ACCTINFOTRNRS.ACCTINFORS.ACCTINFO;
+ expect(Array.isArray(acctInfo)).toBe(true);
+ expect(acctInfo.length).toBe(3); // Checking, savings, and credit card
+
+ // Verify checking account
+ const checkingAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'CHECKING');
+ expect(checkingAccount).toBeDefined();
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('5555666677');
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.BANKID).toBe('322271627');
+
+ // Verify savings account
+ const savingsAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'SAVINGS');
+ expect(savingsAccount).toBeDefined();
+ expect(savingsAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('5555666688');
+
+ // Verify credit card account
+ const creditCardAccount = acctInfo.find(acc => acc.CCACCTINFO);
+ expect(creditCardAccount).toBeDefined();
+ expect(creditCardAccount.CCACCTINFO.CCACCTFROM.ACCTID).toBe('4111111111111111');
+ expect(creditCardAccount.CCACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ done();
+ });
+ });
+ });
+
+ describe('Chase-Specific Error Handling', () => {
+ it('should handle Chase rate limiting', done => {
+ const rateLimitResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 2000
+ ERROR
+ GENERAL ERROR - TOO MANY REQUESTS
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(429, rateLimitResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle Chase maintenance windows', done => {
+ const maintenanceResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 2025
+ ERROR
+ SYSTEM TEMPORARILY UNAVAILABLE
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, maintenanceResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('2025');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('SYSTEM TEMPORARILY UNAVAILABLE');
+ done();
+ });
+ });
+
+ it('should handle invalid date range errors', done => {
+ const dateRangeError = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:103
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ chase-date-error
+
+ 2020
+ ERROR
+ INVALID DATE RANGE
+
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, dateRangeError, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ // Test with invalid date range (end before start)
+ banking.getStatement({ start: 20241201, end: 20241101 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('2020');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID DATE RANGE');
+ done();
+ });
+ });
+ });
+
+ describe('Chase OFX Version Compatibility', () => {
+ it('should work with OFX version 103 (required by Chase)', done => {
+ const chaseWithV103 = new Banking({
+ ...bankConfigs.chase,
+ ofxVer: '103'
+ });
+
+ const scope = nock('https://ofx.chase.com')
+ .post('/', body => {
+ expect(body).toContain('VERSION:103');
+ return true;
+ })
+ .reply(200, chaseStatementResponse);
+
+ chaseWithV103.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle version mismatch gracefully', done => {
+ const versionErrorResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 10015
+ ERROR
+ INVALID OFX VERSION
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://ofx.chase.com').post('/').reply(200, versionErrorResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ const chaseWithOldVersion = new Banking({
+ ...bankConfigs.chase,
+ ofxVer: '102'
+ });
+
+ chaseWithOldVersion.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('10015');
+ done();
+ });
+ });
+ });
+});
diff --git a/test/integration/connection-pool.test.js b/test/integration/connection-pool.test.js
new file mode 100644
index 0000000..7233243
--- /dev/null
+++ b/test/integration/connection-pool.test.js
@@ -0,0 +1,367 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+
+describe('Connection Pool Integration Tests', () => {
+ let banking;
+ let originalPoolConfig;
+
+ beforeEach(() => {
+ // Store original pool config
+ originalPoolConfig = Banking.getPoolMetrics();
+
+ // Reset pool configuration
+ Banking.destroyPool();
+
+ banking = new Banking({
+ fid: 3001,
+ fidOrg: 'Test Bank',
+ url: 'https://testbank.example.com/ofx',
+ bankId: '123456789',
+ user: 'testuser',
+ password: 'testpass',
+ accId: '1234567890',
+ accType: 'CHECKING'
+ });
+ });
+
+ afterEach(() => {
+ Banking.destroyPool();
+ nock.cleanAll();
+ });
+
+ describe('Pool Configuration', () => {
+ it('should use default pool configuration', () => {
+ const config = Banking.configurePool();
+
+ expect(config.maxSockets).toBe(5);
+ expect(config.maxFreeSockets).toBe(2);
+ expect(config.keepAlive).toBe(true);
+ expect(config.keepAliveMsecs).toBe(30000);
+ expect(config.timeout).toBe(60000);
+ expect(config.maxRetries).toBe(3);
+ expect(config.enableMetrics).toBe(true);
+ });
+
+ it('should accept custom pool configuration', () => {
+ const customConfig = {
+ maxSockets: 10,
+ keepAlive: false,
+ timeout: 120000,
+ maxRetries: 5,
+ enableMetrics: false
+ };
+
+ const config = Banking.configurePool(customConfig);
+
+ expect(config.maxSockets).toBe(10);
+ expect(config.keepAlive).toBe(false);
+ expect(config.timeout).toBe(120000);
+ expect(config.maxRetries).toBe(5);
+ expect(config.enableMetrics).toBe(false);
+ });
+
+ it('should return null metrics when pool not initialized', () => {
+ Banking.destroyPool();
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics).toBeNull();
+ });
+ });
+
+ describe('Pool Metrics', () => {
+ beforeEach(() => {
+ Banking.configurePool({
+ maxSockets: 3,
+ enableMetrics: true
+ });
+ });
+
+ it('should track basic request metrics', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').reply(200, mockResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const metrics = Banking.getPoolMetrics();
+
+ expect(metrics.totalRequests).toBe(1);
+ expect(metrics.poolMisses).toBe(1); // First request creates new agent
+ expect(metrics.poolHits).toBe(0);
+ expect(metrics.errors).toBe(0);
+ expect(metrics.agentCount).toBe(1);
+ expect(metrics.averageResponseTime).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+
+ it('should track pool hits on subsequent requests', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').times(2).reply(200, mockResponse);
+
+ // First request
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ // Second request to same host should reuse agent
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.totalRequests).toBe(2);
+ expect(metrics.poolMisses).toBe(1); // Only first request creates agent
+ expect(metrics.poolHits).toBe(1); // Second request reuses agent
+
+ done();
+ });
+ });
+ });
+
+ it('should track errors in metrics', done => {
+ nock('https://testbank.example.com').post('/ofx').reply(500, 'Internal Server Error');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.totalRequests).toBe(1);
+ expect(metrics.errors).toBeGreaterThan(0); // Should track the error
+
+ done();
+ });
+ });
+ });
+
+ describe('Connection Reuse', () => {
+ beforeEach(() => {
+ Banking.configurePool({
+ maxSockets: 5,
+ keepAlive: true,
+ enableMetrics: true
+ });
+ });
+
+ it('should reuse connections for same host', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').times(3).reply(200, mockResponse);
+
+ let completedRequests = 0;
+ const checkCompletion = () => {
+ completedRequests++;
+ if (completedRequests === 3) {
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.totalRequests).toBe(3);
+ expect(metrics.agentCount).toBe(1); // Should only create one agent
+ expect(metrics.poolHits).toBe(2); // 2nd and 3rd requests reuse agent
+ done();
+ }
+ };
+
+ // Make three concurrent requests
+ banking.getStatement({ start: 20241101, end: 20241201 }, checkCompletion);
+ banking.getStatement({ start: 20241101, end: 20241201 }, checkCompletion);
+ banking.getStatement({ start: 20241101, end: 20241201 }, checkCompletion);
+ });
+
+ it('should create separate agents for different hosts', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').reply(200, mockResponse);
+
+ nock('https://anotherbank.example.com').post('/ofx').reply(200, mockResponse);
+
+ const anotherBanking = new Banking({
+ ...banking.opts,
+ url: 'https://anotherbank.example.com/ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ anotherBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.totalRequests).toBe(2);
+ expect(metrics.agentCount).toBe(2); // Should create agents for both hosts
+ expect(metrics.poolMisses).toBe(2); // Both are first requests to their hosts
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Retry Logic', () => {
+ beforeEach(() => {
+ Banking.configurePool({
+ maxRetries: 2,
+ retryDelay: 100, // Short delay for testing
+ enableMetrics: true
+ });
+ });
+
+ it('should retry on server errors', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').reply(500, 'Internal Server Error').post('/ofx').reply(200, mockResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false); // Should succeed after retry
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.retries).toBe(1); // Should have retried once
+
+ done();
+ });
+ });
+
+ it('should retry on network errors', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').replyWithError('ECONNRESET').post('/ofx').reply(200, mockResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false); // Should succeed after retry
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.retries).toBe(1); // Should have retried once
+
+ done();
+ });
+ });
+
+ it('should fail after max retries', done => {
+ nock('https://testbank.example.com')
+ .post('/ofx')
+ .times(3) // Initial + 2 retries
+ .reply(500, 'Internal Server Error');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy(); // Should fail after max retries
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.retries).toBe(2); // Should have retried max times
+ expect(metrics.errors).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('Timeout Handling', () => {
+ beforeEach(() => {
+ Banking.configurePool({
+ timeout: 1000, // 1 second timeout for testing
+ maxRetries: 1,
+ enableMetrics: true
+ });
+ });
+
+ it('should timeout slow requests', done => {
+ nock('https://testbank.example.com')
+ .post('/ofx')
+ .delay(2000) // 2 second delay
+ .reply(200, 'Too late');
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ETIMEDOUT');
+ expect(elapsedTime).toBeGreaterThan(1000);
+ expect(elapsedTime).toBeLessThan(4000); // Should timeout before 4s (with retries)
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics.errors).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('Legacy Mode Support', () => {
+ it('should use legacy TLS sockets when pooling disabled', done => {
+ const bankingWithoutPool = new Banking({
+ ...banking.opts,
+ usePooling: false
+ });
+
+ const mockResponse = 'HTTP/1.1 200 OK\r\n\r\nOFXHEADER:100\r\nDATA:OFXSGML\r\n';
+
+ nock('https://testbank.example.com').post('/ofx').reply(200, mockResponse);
+
+ bankingWithoutPool.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ // Pool metrics should not be affected since pooling was disabled
+ const metrics = Banking.getPoolMetrics();
+ // Metrics might be null if no other pooled requests were made
+ if (metrics) {
+ expect(metrics.totalRequests).toBe(0);
+ }
+
+ done();
+ });
+ });
+ });
+
+ describe('Pool Lifecycle', () => {
+ it('should properly initialize and destroy pool', () => {
+ expect(Banking.getPoolMetrics()).toBeNull();
+
+ Banking.configurePool();
+ // Pool is created immediately when configured
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics).not.toBeNull();
+ expect(metrics.totalRequests).toBe(0);
+
+ Banking.destroyPool();
+ expect(Banking.getPoolMetrics()).toBeNull();
+ });
+
+ it('should handle multiple destroy calls gracefully', () => {
+ Banking.configurePool();
+ Banking.destroyPool();
+ Banking.destroyPool(); // Should not throw
+ expect(Banking.getPoolMetrics()).toBeNull();
+ });
+ });
+
+ describe('Banking-Specific Optimizations', () => {
+ beforeEach(() => {
+ Banking.configurePool({
+ maxSockets: 5,
+ keepAlive: true,
+ keepAliveMsecs: 30000,
+ enableMetrics: true
+ });
+ });
+
+ it('should handle OFX-specific headers correctly', done => {
+ const mockResponse = 'HTTP/1.1 200 OK\r\nContent-Type: application/x-ofx\r\n\r\nOFXHEADER:100\r\n';
+
+ nock('https://testbank.example.com')
+ .post('/ofx')
+ .matchHeader('Content-Type', 'application/x-ofx')
+ .matchHeader('Content-Length', /\d+/)
+ .reply(200, mockResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should maintain SSL security for banking connections', () => {
+ const config = Banking.configurePool();
+
+ expect(config.secureProtocol).toBe('TLSv1_2_method');
+ expect(config.rejectUnauthorized).toBe(true);
+ });
+ });
+});
diff --git a/test/integration/discover.test.js b/test/integration/discover.test.js
new file mode 100644
index 0000000..0196f5d
--- /dev/null
+++ b/test/integration/discover.test.js
@@ -0,0 +1,307 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { discoverCardStatementResponse, invalidCredentialsResponse, malformedOFXResponse, bankConfigs } from '../fixtures/responses.js';
+
+describe('Discover Financial Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.discover);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve credit card statement', done => {
+ // Mock the OFX server response
+ nock('https://ofx.discovercard.com').post('/').reply(200, discoverCardStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.ORG).toBe('Discover Financial Services');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.FID).toBe('7101');
+
+ // Verify credit card statement data (different structure than bank statements)
+ expect(ofx.CREDITCARDMSGSRSV1).toBeDefined();
+ const ccstmtrs = ofx.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS;
+ expect(ccstmtrs.CURDEF).toBe('USD');
+ expect(ccstmtrs.CCACCTFROM.ACCTID).toBe('6011123456789012');
+
+ // Verify transactions
+ const transactions = ccstmtrs.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBeGreaterThan(0);
+
+ // Verify specific transaction data
+ const amazonTransaction = transactions.find(t => t.NAME === 'AMAZON.COM');
+ expect(amazonTransaction).toBeDefined();
+ expect(amazonTransaction.TRNTYPE).toBe('DEBIT');
+ expect(amazonTransaction.TRNAMT).toBe('-89.99');
+ expect(amazonTransaction.MEMO).toBe('ONLINE PURCHASE');
+
+ const paymentTransaction = transactions.find(t => t.NAME === 'PAYMENT THANK YOU');
+ expect(paymentTransaction).toBeDefined();
+ expect(paymentTransaction.TRNTYPE).toBe('CREDIT');
+ expect(paymentTransaction.TRNAMT).toBe('100.00');
+
+ // Verify balance information (credit cards show negative balance when you owe money)
+ expect(ccstmtrs.LEDGERBAL).toBeDefined();
+ expect(ccstmtrs.LEDGERBAL.BALAMT).toBe('-35.66');
+
+ done();
+ });
+ });
+
+ it('should handle invalid credentials for Discover', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('INVALID SIGNON');
+ done();
+ });
+ });
+
+ it('should handle Discover-specific headers correctly', done => {
+ const discoverWithHeaders = new Banking({
+ ...bankConfigs.discover,
+ headers: ['Content-Type', 'Host', 'Content-Length', 'Connection']
+ });
+
+ const scope = nock('https://ofx.discovercard.com')
+ .post('/', body => {
+ // Verify Discover-specific OFX request
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('7101');
+ expect(body).toContain('6011123456789012');
+ expect(body).not.toContain(''); // Credit cards don't use bank routing numbers
+ return true;
+ })
+ .reply(200, discoverCardStatementResponse);
+
+ discoverWithHeaders.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle HTTP errors from Discover servers', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(503, 'Service Unavailable');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle connection timeouts', done => {
+ nock('https://ofx.discovercard.com').post('/').delay(10000).reply(200, discoverCardStatementResponse);
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(elapsedTime).toBeGreaterThan(1000);
+ done();
+ });
+ });
+
+ it('should handle malformed responses from Discover', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, malformedOFXResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should successfully retrieve Discover card account info', done => {
+ const discoverAccountResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-discover-accounts
+
+
+
+
+
+ 0
+ INFO
+
+ 20241201120000.000[-5:EST]
+ ENG
+
+ Discover Financial Services
+ 7101
+
+
+
+
+
+ discover-accounts-uid-456
+
+ 0
+ INFO
+
+
+ 20241201120000.000
+
+
+
+ 6011123456789012
+
+ Y
+ N
+ N
+ ACTIVE
+
+
+
+
+
+ `;
+
+ nock('https://ofx.discovercard.com').post('/').reply(200, discoverAccountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ const acctInfo = res.body.OFX.SIGNUPMSGSRSV1.ACCTINFOTRNRS.ACCTINFORS.ACCTINFO;
+ expect(acctInfo.CCACCTINFO).toBeDefined();
+ expect(acctInfo.CCACCTINFO.CCACCTFROM.ACCTID).toBe('6011123456789012');
+ expect(acctInfo.CCACCTINFO.SVCSTATUS).toBe('ACTIVE');
+ expect(acctInfo.CCACCTINFO.SUPTXDL).toBe('Y'); // Supports transaction download
+
+ done();
+ });
+ });
+
+ it('should handle authentication error for Discover account list', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ done();
+ });
+ });
+ });
+
+ describe('Credit Card Specific Features', () => {
+ it('should correctly parse credit card transaction types', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, discoverCardStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const transactions = res.body.OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.STMTTRN;
+
+ // Verify transaction types common to credit cards
+ const debitTransactions = transactions.filter(t => t.TRNTYPE === 'DEBIT');
+ expect(debitTransactions.length).toBeGreaterThan(0);
+
+ const creditTransactions = transactions.filter(t => t.TRNTYPE === 'CREDIT');
+ expect(creditTransactions.length).toBeGreaterThan(0);
+
+ // All debit amounts should be negative (charges)
+ debitTransactions.forEach(t => {
+ expect(parseFloat(t.TRNAMT)).toBeLessThan(0);
+ });
+
+ // All credit amounts should be positive (payments)
+ creditTransactions.forEach(t => {
+ expect(parseFloat(t.TRNAMT)).toBeGreaterThan(0);
+ });
+
+ done();
+ });
+ });
+
+ it('should handle zero balance scenarios', done => {
+ const zeroBalanceResponse = discoverCardStatementResponse.replace('-35.66', '0.00');
+
+ nock('https://ofx.discovercard.com').post('/').reply(200, zeroBalanceResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res.body.OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.LEDGERBAL.BALAMT).toBe('0.00');
+ done();
+ });
+ });
+
+ it('should validate request format for credit card accounts', done => {
+ const scope = nock('https://ofx.discovercard.com')
+ .post('/', body => {
+ // Verify credit card-specific request structure
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain(''); // Credit card statement request
+ expect(body).toContain('');
+ expect(body).toContain('6011123456789012');
+ expect(body).not.toContain(''); // Should not contain bank account structure
+ expect(body).not.toContain(''); // Credit cards don't use routing numbers
+ return true;
+ })
+ .reply(200, discoverCardStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('Date Range Handling', () => {
+ it('should handle different date formats', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, discoverCardStatementResponse);
+
+ // Test with string dates
+ banking.getStatement({ start: '20241101', end: '20241201' }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle year-long date ranges', done => {
+ nock('https://ofx.discovercard.com').post('/').reply(200, discoverCardStatementResponse);
+
+ banking.getStatement({ start: 20240101, end: 20241231 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/test/integration/edge-cases.test.js b/test/integration/edge-cases.test.js
new file mode 100644
index 0000000..4ea08f5
--- /dev/null
+++ b/test/integration/edge-cases.test.js
@@ -0,0 +1,658 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { wellsFargoStatementResponse, discoverCardStatementResponse, bankConfigs } from '../fixtures/responses.js';
+
+describe('Edge Cases and Boundary Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ describe('Date Range Edge Cases', () => {
+ it('should handle same start and end date', done => {
+ const singleDayResponse = wellsFargoStatementResponse
+ .replace('20241101120000.000', '20241115120000.000')
+ .replace('20241201120000.000', '20241115120000.000');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, singleDayResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241115, end: 20241115 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.DTSTART).toBe('20241115120000.000');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.DTEND).toBe('20241115120000.000');
+ done();
+ });
+ });
+
+ it('should handle maximum allowed date range', done => {
+ // Most banks limit to 2-3 years of data
+ const maxRangeResponse = wellsFargoStatementResponse.replace('20241101120000.000', '20220101120000.000');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, maxRangeResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20220101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle future dates', done => {
+ const futureDateResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ future-date-error
+
+ 2019
+ ERROR
+ INVALID DATE - FUTURE DATES NOT ALLOWED
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, futureDateResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20250101, end: 20251231 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('2019');
+ done();
+ });
+ });
+
+ it('should handle very old dates', done => {
+ const oldDateResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ old-date-error
+
+ 2018
+ ERROR
+ DATA NOT AVAILABLE FOR REQUESTED DATE RANGE
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, oldDateResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 19900101, end: 19901231 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('2018');
+ done();
+ });
+ });
+
+ it('should handle leap year dates', done => {
+ const leapYearResponse = wellsFargoStatementResponse
+ .replace('20241101120000.000', '20240228120000.000')
+ .replace('20241201120000.000', '20240229120000.000');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, leapYearResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20240228, end: 20240229 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+ });
+
+ describe('Account Configuration Edge Cases', () => {
+ it('should handle extremely long account numbers', done => {
+ const longAcctBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ accId: '1234567890123456789012345678901234567890'
+ });
+
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ expect(body).toContain('1234567890123456789012345678901234567890');
+ return true;
+ })
+ .reply(200, wellsFargoStatementResponse.replace('1234567890', '1234567890123456789012345678901234567890'));
+
+ longAcctBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle special characters in account IDs', done => {
+ const specialCharBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ accId: 'ACCT-123_456.789',
+ user: 'test.user+123',
+ password: 'p@ssw0rd!'
+ });
+
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ expect(body).toContain('ACCT-123_456.789');
+ expect(body).toContain('test.user+123');
+ expect(body).toContain('p@ssw0rd!');
+ return true;
+ })
+ .reply(200, wellsFargoStatementResponse.replace('1234567890', 'ACCT-123_456.789'));
+
+ specialCharBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle empty or null account fields', done => {
+ const emptyFieldBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ bankId: '', // Empty bank ID
+ brokerId: null, // Null broker ID
+ clientId: undefined // Undefined client ID
+ });
+
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ expect(body).toContain('');
+ expect(body).not.toContain('');
+ expect(body).not.toContain('');
+ return true;
+ })
+ .reply(200, wellsFargoStatementResponse);
+
+ emptyFieldBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle very long passwords', done => {
+ const longPasswordBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ password: 'a'.repeat(256) // 256 character password
+ });
+
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ expect(body).toContain('' + 'a'.repeat(256));
+ return true;
+ })
+ .reply(200, wellsFargoStatementResponse);
+
+ longPasswordBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('Response Size and Content Edge Cases', () => {
+ it('should handle responses with no transactions', done => {
+ const noTransactionsResponse = wellsFargoStatementResponse
+ .replace(/[\s\S]*?<\/STMTTRN>/g, '')
+ .replace('', '');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, noTransactionsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle responses with single transaction', done => {
+ const singleTransactionResponse = wellsFargoStatementResponse.replace(
+ /[\s\S]*?<\/STMTTRN>/g,
+ `
+ DEBIT
+ 20241115120000.000
+ -50.00
+ WF202411150001
+ SINGLE TRANSACTION
+ ONLY TRANSACTION IN PERIOD
+ `
+ );
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, singleTransactionResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ // Could be an array with one element or a single object
+ if (Array.isArray(transactions)) {
+ expect(transactions.length).toBe(1);
+ expect(transactions[0].NAME).toBe('SINGLE TRANSACTION');
+ } else {
+ expect(transactions.NAME).toBe('SINGLE TRANSACTION');
+ }
+ done();
+ });
+ });
+
+ it('should handle responses with extremely large transaction lists', done => {
+ // Generate 1000 transactions
+ let transactionList = '';
+ for (let i = 1; i <= 1000; i++) {
+ transactionList += `
+
+ DEBIT
+ 20241115120000.000
+ -${i}.00
+ WF20241115${i.toString().padStart(4, '0')}
+ TRANSACTION ${i}
+ GENERATED TRANSACTION ${i}
+ `;
+ }
+
+ const largeTransactionResponse = wellsFargoStatementResponse.replace(/[\s\S]*?<\/STMTTRN>/g, transactionList);
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, largeTransactionResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBe(1000);
+ expect(transactions[0].NAME).toBe('TRANSACTION 1');
+ expect(transactions[999].NAME).toBe('TRANSACTION 1000');
+ done();
+ });
+ });
+
+ it('should handle transactions with missing or empty fields', done => {
+ const incompleteTransactionResponse = wellsFargoStatementResponse.replace(
+ /[\s\S]*?<\/STMTTRN>/g,
+ `
+ DEBIT
+ 20241115120000.000
+ -100.00
+ WF202411150001
+
+
+
+
+ CREDIT
+ 20241116120000.000
+ 200.00
+ WF202411160001
+ DEPOSIT
+ `
+ );
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, incompleteTransactionResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBe(2);
+
+ // First transaction has empty name and memo
+ expect(transactions[0].NAME).toBe('');
+ expect(transactions[0].MEMO).toBe('');
+
+ // Second transaction missing memo field entirely
+ expect(transactions[1].NAME).toBe('DEPOSIT');
+ expect(transactions[1].MEMO).toBeUndefined();
+
+ done();
+ });
+ });
+ });
+
+ describe('Currency and Amount Edge Cases', () => {
+ it('should handle zero-amount transactions', done => {
+ const zeroAmountResponse = wellsFargoStatementResponse.replace('-150.00', '0.00');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, zeroAmountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ const zeroTransaction = transactions.find(t => t.TRNAMT === '0.00');
+ expect(zeroTransaction).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle very large monetary amounts', done => {
+ const largeAmountResponse = wellsFargoStatementResponse
+ .replace('2500.00', '999999999.99')
+ .replace('2338.00', '999999999.99');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, largeAmountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const stmtrs = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS;
+ expect(stmtrs.LEDGERBAL.BALAMT).toBe('999999999.99');
+
+ const transactions = stmtrs.BANKTRANLIST.STMTTRN;
+ const largeTransaction = transactions.find(t => t.TRNAMT === '999999999.99');
+ expect(largeTransaction).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle fractional cent amounts', done => {
+ const fractionalAmountResponse = wellsFargoStatementResponse
+ .replace('-150.00', '-150.001')
+ .replace('2500.00', '2500.999');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, fractionalAmountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ const fractionalDebit = transactions.find(t => t.TRNAMT === '-150.001');
+ const fractionalCredit = transactions.find(t => t.TRNAMT === '2500.999');
+ expect(fractionalDebit).toBeDefined();
+ expect(fractionalCredit).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle non-USD currencies', done => {
+ const euroCurrencyResponse = wellsFargoStatementResponse.replace('USD', 'EUR');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, euroCurrencyResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.CURDEF).toBe('EUR');
+ done();
+ });
+ });
+ });
+
+ describe('Character Encoding and Special Characters', () => {
+ it('should handle Unicode characters in transaction descriptions', done => {
+ const unicodeResponse = wellsFargoStatementResponse
+ .replace('GROCERY STORE PURCHASE', 'CafĆ© München - ā¬50.00 Purchase')
+ .replace('WHOLE FOODS MARKET', 'StraĆe ā123, München, Deutschland');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, unicodeResponse, {
+ 'Content-Type': 'application/x-ofx; charset=utf-8'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ const unicodeTransaction = transactions.find(t => t.NAME.includes('CafƩ'));
+ expect(unicodeTransaction).toBeDefined();
+ expect(unicodeTransaction.NAME).toBe('CafĆ© München - ā¬50.00 Purchase');
+ expect(unicodeTransaction.MEMO).toBe('StraĆe ā123, München, Deutschland');
+ done();
+ });
+ });
+
+ it('should handle HTML entities in responses', done => {
+ const htmlEntityResponse = wellsFargoStatementResponse
+ .replace('GROCERY STORE PURCHASE', 'AT&T Payment <Auto>')
+ .replace('WHOLE FOODS MARKET', '"Monthly Service" & Fees');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, htmlEntityResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ const entityTransaction = transactions.find(t => t.NAME.includes('AT&T'));
+ expect(entityTransaction).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle very long transaction descriptions', done => {
+ const longDescription = 'A'.repeat(1000);
+ const longDescriptionResponse = wellsFargoStatementResponse
+ .replace('GROCERY STORE PURCHASE', `${longDescription}`)
+ .replace('WHOLE FOODS MARKET', `${longDescription}`);
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, longDescriptionResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+ const longTransaction = transactions.find(t => t.NAME === longDescription);
+ expect(longTransaction).toBeDefined();
+ expect(longTransaction.MEMO).toBe(longDescription);
+ done();
+ });
+ });
+ });
+
+ describe('Time Zone and Date Format Edge Cases', () => {
+ it('should handle different time zone formats', done => {
+ const timeZoneResponse = wellsFargoStatementResponse
+ .replace('20241201120000.000[-8:PST]', '20241201120000.000[+5:EST]')
+ .replace('20241115120000.000', '20241115120000.000[-8:PST]');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, timeZoneResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.DTSERVER).toBe('20241201120000.000[+5:EST]');
+ done();
+ });
+ });
+
+ it('should handle dates without time components', done => {
+ const noTimeResponse = wellsFargoStatementResponse.replace(/\d{8}120000\.000/g, match => match.substring(0, 8));
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, noTimeResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+ });
+
+ describe('Network Behavior Edge Cases', () => {
+ it('should handle partial response chunks', done => {
+ const responseChunks = [
+ 'OFXHEADER:100\nDATA:OFXSGML\nVERSION:102\n',
+ 'SECURITY:NONE\nENCODING:USASCII\n',
+ wellsFargoStatementResponse.substring(100)
+ ];
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .reply(function (uri, requestBody) {
+ const response = responseChunks.join('');
+ return [200, response, { 'Content-Type': 'application/x-ofx' }];
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ if (!err) {
+ expect(res).toBeDefined();
+ }
+ done();
+ });
+ });
+
+ it('should handle slow response streams', done => {
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(1500) // Slow but not timeout
+ .reply(200, wellsFargoStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle responses with incorrect content-type', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoStatementResponse, {
+ 'Content-Type': 'text/plain' // Wrong content type
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ // Should still parse OFX regardless of content-type
+ if (!err) {
+ expect(res).toBeDefined();
+ }
+ done();
+ });
+ });
+
+ it('should handle responses with additional headers', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoStatementResponse, {
+ 'Content-Type': 'application/x-ofx',
+ 'X-Custom-Header': 'BankSpecificValue',
+ 'Cache-Control': 'no-cache',
+ 'Set-Cookie': 'SessionId=abc123; HttpOnly'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.header).toBeDefined();
+ done();
+ });
+ });
+ });
+
+ describe('Concurrent Request Handling', () => {
+ it('should handle multiple simultaneous requests', done => {
+ // Set up multiple responses
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').times(3).reply(200, wellsFargoStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ let completedRequests = 0;
+ const checkCompletion = () => {
+ completedRequests++;
+ if (completedRequests === 3) {
+ done();
+ }
+ };
+
+ // Make three simultaneous requests
+ banking.getStatement({ start: 20241101, end: 20241110 }, (err, res) => {
+ expect(err).toBe(false);
+ checkCompletion();
+ });
+
+ banking.getStatement({ start: 20241111, end: 20241120 }, (err, res) => {
+ expect(err).toBe(false);
+ checkCompletion();
+ });
+
+ banking.getStatement({ start: 20241121, end: 20241130 }, (err, res) => {
+ expect(err).toBe(false);
+ checkCompletion();
+ });
+ });
+ });
+});
diff --git a/test/integration/error-handling.test.js b/test/integration/error-handling.test.js
new file mode 100644
index 0000000..946ddf6
--- /dev/null
+++ b/test/integration/error-handling.test.js
@@ -0,0 +1,700 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { invalidCredentialsResponse, accountNotFoundResponse, malformedOFXResponse, bankConfigs } from '../fixtures/responses.js';
+
+describe('Comprehensive Error Handling Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ describe('Network and Connection Errors', () => {
+ it('should handle DNS resolution failures', done => {
+ const badUrlBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ url: 'https://nonexistent-bank-domain.invalid'
+ });
+
+ nock('https://nonexistent-bank-domain.invalid').post('/').replyWithError({ code: 'ENOTFOUND', message: 'getaddrinfo ENOTFOUND' });
+
+ badUrlBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ENOTFOUND');
+ done();
+ });
+ });
+
+ it('should handle connection refused errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'ECONNREFUSED', message: 'Connection refused' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ECONNREFUSED');
+ done();
+ });
+ });
+
+ it('should handle connection reset errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'ECONNRESET', message: 'Connection reset by peer' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ECONNRESET');
+ done();
+ });
+ });
+
+ it('should handle network unreachable errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'ENETUNREACH', message: 'Network is unreachable' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ENETUNREACH');
+ done();
+ });
+ });
+
+ it('should handle socket timeout errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').delay(30000).reply(200, 'Should never reach here');
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(elapsedTime).toBeGreaterThan(2000);
+ done();
+ });
+ });
+ });
+
+ describe('HTTP Status Code Errors', () => {
+ it('should handle 400 Bad Request', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(400, 'Bad Request - Invalid OFX Format');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 401 Unauthorized', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(401, 'Unauthorized - Invalid Credentials');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 403 Forbidden', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(403, 'Forbidden - Access Denied');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 404 Not Found', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(404, 'Not Found - OFX Endpoint Not Available');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 429 Too Many Requests', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(429, 'Too Many Requests', {
+ 'Retry-After': '300'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 500 Internal Server Error', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(500, 'Internal Server Error');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 502 Bad Gateway', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(502, 'Bad Gateway');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 503 Service Unavailable', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(503, 'Service Unavailable - Maintenance Mode');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle 504 Gateway Timeout', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(504, 'Gateway Timeout');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('OFX Protocol Errors', () => {
+ it('should handle general OFX errors', done => {
+ const generalErrorResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 2000
+ ERROR
+ GENERAL ERROR
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, generalErrorResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('2000');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.SEVERITY).toBe('ERROR');
+ done();
+ });
+ });
+
+ it('should handle OFX version unsupported errors', done => {
+ const versionErrorResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 10015
+ ERROR
+ UNSUPPORTED OFX VERSION
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, versionErrorResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('10015');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('UNSUPPORTED OFX VERSION');
+ done();
+ });
+ });
+
+ it('should handle invalid user credentials', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('INVALID SIGNON');
+ done();
+ });
+ });
+
+ it('should handle locked account errors', done => {
+ const lockedAccountResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 15501
+ ERROR
+ CUSTOMER ACCOUNT ALREADY IN USE
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, lockedAccountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15501');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('CUSTOMER ACCOUNT ALREADY IN USE');
+ done();
+ });
+ });
+
+ it('should handle expired password errors', done => {
+ const expiredPasswordResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 15505
+ ERROR
+ PASSWORD EXPIRED
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, expiredPasswordResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15505');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('PASSWORD EXPIRED');
+ done();
+ });
+ });
+ });
+
+ describe('Account and Transaction Errors', () => {
+ it('should handle invalid account number', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, accountNotFoundResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10500');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID ACCOUNT NUMBER');
+ done();
+ });
+ });
+
+ it('should handle account restrictions', done => {
+ const accountRestrictedResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ restricted-account-error
+
+ 10401
+ ERROR
+ ACCOUNT RESTRICTED - CONTACT BANK
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, accountRestrictedResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10401');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('ACCOUNT RESTRICTED - CONTACT BANK');
+ done();
+ });
+ });
+
+ it('should handle invalid date range errors', done => {
+ const invalidDateRangeResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ invalid-date-range-error
+
+ 2020
+ ERROR
+ INVALID DATE RANGE
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, invalidDateRangeResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20251201, end: 20241101 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('2020');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID DATE RANGE');
+ done();
+ });
+ });
+
+ it('should handle request too large errors', done => {
+ const requestTooLargeResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ request-too-large-error
+
+ 13000
+ ERROR
+ REQUEST TOO LARGE - REDUCE DATE RANGE
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, requestTooLargeResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ // Request 10 years of data
+ banking.getStatement({ start: 20140101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('13000');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('REQUEST TOO LARGE - REDUCE DATE RANGE');
+ done();
+ });
+ });
+ });
+
+ describe('Data Parsing and Format Errors', () => {
+ it('should handle completely malformed OFX', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, malformedOFXResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle missing OFX header', done => {
+ const noHeaderResponse = `
+
+
+
+ 0
+ INFO
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, noHeaderResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ // Should handle gracefully or throw appropriate error
+ if (err) {
+ expect(err).toBeTruthy();
+ } else {
+ expect(res).toBeDefined();
+ }
+ done();
+ });
+ });
+
+ it('should handle corrupted XML structure', done => {
+ const corruptedXMLResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ corrupted-xml
+
+ 0
+ INFO
+ SUCCESS
+
+
+ USD
+
+ 123456789
+ 987654321
+ CHECKING
+
+
+ 20241101
+ 20241201
+
+ DEBIT
+ 20241115
+ -100.00
+ corrupt123
+ BROKEN TRANSACTION
+
+ TEST
+
+
+
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, corruptedXMLResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ // Should either handle gracefully or throw parsing error
+ if (err) {
+ expect(err).toBeTruthy();
+ } else {
+ expect(res).toBeDefined();
+ }
+ done();
+ });
+ });
+
+ it('should handle empty response body', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, '', {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle non-OFX response (HTML error page)', done => {
+ const htmlErrorPage = `
+
+
+ Service Unavailable
+
+
+ 503 Service Unavailable
+ The OFX service is temporarily unavailable. Please try again later.
+
+`;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, htmlErrorPage, {
+ 'Content-Type': 'text/html'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('SSL and Security Errors', () => {
+ it('should handle SSL certificate verification errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({
+ code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
+ message: 'Unable to verify the first certificate'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('UNABLE_TO_VERIFY_LEAF_SIGNATURE');
+ done();
+ });
+ });
+
+ it('should handle SSL handshake failures', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({
+ code: 'EPROTO',
+ message: 'SSL handshake failed'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('EPROTO');
+ done();
+ });
+ });
+
+ it('should handle self-signed certificate errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({
+ code: 'DEPTH_ZERO_SELF_SIGNED_CERT',
+ message: 'Self signed certificate'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('DEPTH_ZERO_SELF_SIGNED_CERT');
+ done();
+ });
+ });
+ });
+
+ describe('Resource and System Errors', () => {
+ it('should handle out of memory errors', done => {
+ // Simulate a very large response that could cause memory issues
+ const hugeResponse = 'OFXHEADER:100\nDATA:OFXSGML\nVERSION:102\n' + 'X'.repeat(50000);
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, hugeResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle file system errors (if applicable)', () => {
+ // This would test scenarios where temporary files can't be written
+ // For now, just ensure the error handling exists
+ expect(() => {
+ const testBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ // Invalid characters in filename if temp files are used
+ user: '../../../etc/passwd',
+ password: 'test'
+ });
+ testBanking.getStatement;
+ }).not.toThrow();
+ });
+ });
+});
diff --git a/test/integration/structured-error-handling.test.js b/test/integration/structured-error-handling.test.js
new file mode 100644
index 0000000..8cb4f31
--- /dev/null
+++ b/test/integration/structured-error-handling.test.js
@@ -0,0 +1,472 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { bankConfigs } from '../fixtures/responses.js';
+
+describe('Structured Error Handling System', () => {
+ let banking;
+
+ beforeEach(() => {
+ // Reset any existing nock interceptors
+ nock.cleanAll();
+ });
+
+ describe('Error Class Inheritance and Structure', () => {
+ it('should have proper error class hierarchy', () => {
+ expect(Banking.BankingError).toBeDefined();
+ expect(Banking.NetworkError).toBeDefined();
+ expect(Banking.ConnectionError).toBeDefined();
+ expect(Banking.TimeoutError).toBeDefined();
+ expect(Banking.AuthenticationError).toBeDefined();
+ expect(Banking.InvalidCredentialsError).toBeDefined();
+ expect(Banking.BankingBusinessError).toBeDefined();
+ expect(Banking.AccountNotFoundError).toBeDefined();
+ expect(Banking.OFXProtocolError).toBeDefined();
+ expect(Banking.MalformedResponseError).toBeDefined();
+ expect(Banking.ConfigurationError).toBeDefined();
+ expect(Banking.InvalidConfigurationError).toBeDefined();
+ });
+
+ it('should create errors with proper inheritance', () => {
+ const connectionError = new Banking.ConnectionError('Test connection error');
+
+ expect(connectionError).toBeInstanceOf(Banking.ConnectionError);
+ expect(connectionError).toBeInstanceOf(Banking.NetworkError);
+ expect(connectionError).toBeInstanceOf(Banking.BankingError);
+ expect(connectionError).toBeInstanceOf(Error);
+ });
+
+ it('should include correlation IDs and structured information', () => {
+ const error = new Banking.BankingError('Test error', {
+ fid: 12345,
+ fidOrg: 'Test Bank',
+ operationType: 'statement'
+ });
+
+ expect(error.correlationId).toBeDefined();
+ expect(error.correlationId).toMatch(/^[A-Za-z0-9]{16}$/);
+ expect(error.timestamp).toBeDefined();
+ expect(error.category).toBe('UNKNOWN');
+ expect(error.bankingContext.fid).toBe(12345);
+ expect(error.bankingContext.fidOrg).toBe('Test Bank');
+ expect(error.bankingContext.operationType).toBe('statement');
+ });
+
+ it('should provide PCI-compliant logging', () => {
+ const error = new Banking.BankingError('Test error', {
+ fid: 12345,
+ url: 'https://username:password@bank.com/ofx?secret=123'
+ });
+
+ const logObj = error.toLogObject();
+
+ // Should sanitize URLs
+ expect(logObj.bankingContext.url).not.toContain('username');
+ expect(logObj.bankingContext.url).not.toContain('password');
+ expect(logObj.bankingContext.url).not.toContain('secret');
+
+ // Should include safe information
+ expect(logObj.correlationId).toBeDefined();
+ expect(logObj.timestamp).toBeDefined();
+ expect(logObj.bankingContext.fid).toBe(12345);
+ });
+ });
+
+ describe('Configuration Validation', () => {
+ it('should throw InvalidConfigurationError for missing config', () => {
+ expect(() => new Banking()).toThrow(Banking.InvalidConfigurationError);
+ expect(() => new Banking()).toThrow('Configuration object is required');
+ });
+
+ it('should throw MissingParameterError for missing required fields', () => {
+ expect(() => new Banking({})).toThrow(Banking.MissingParameterError);
+ expect(() => new Banking({})).toThrow("Required parameter 'fid' is missing");
+ });
+
+ it('should throw InvalidConfigurationError for invalid FID', () => {
+ expect(
+ () =>
+ new Banking({
+ fid: 'invalid',
+ url: 'https://bank.com',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'CHECKING'
+ })
+ ).toThrow(Banking.InvalidConfigurationError);
+ expect(
+ () =>
+ new Banking({
+ fid: 'invalid',
+ url: 'https://bank.com',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'CHECKING'
+ })
+ ).toThrow('FID must be a positive number');
+ });
+
+ it('should throw InvalidConfigurationError for invalid URL', () => {
+ expect(
+ () =>
+ new Banking({
+ fid: 12345,
+ url: 'invalid-url',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'CHECKING'
+ })
+ ).toThrow(Banking.InvalidConfigurationError);
+ expect(
+ () =>
+ new Banking({
+ fid: 12345,
+ url: 'invalid-url',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'CHECKING'
+ })
+ ).toThrow('Invalid URL format');
+ });
+
+ it('should throw InvalidConfigurationError for invalid account type', () => {
+ expect(
+ () =>
+ new Banking({
+ fid: 12345,
+ url: 'https://bank.com',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'INVALID'
+ })
+ ).toThrow(Banking.InvalidConfigurationError);
+ expect(
+ () =>
+ new Banking({
+ fid: 12345,
+ url: 'https://bank.com',
+ user: 'user',
+ password: 'pass',
+ accId: '123',
+ accType: 'INVALID'
+ })
+ ).toThrow('Invalid account type');
+ });
+ });
+
+ describe('Network Error Classification', () => {
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ it('should classify DNS errors correctly', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'ENOTFOUND', message: 'getaddrinfo ENOTFOUND' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.DNSError);
+ expect(err).toBeInstanceOf(Banking.NetworkError);
+ expect(err.code).toBe('DNS_ERROR');
+ expect(err.retryable).toBe(false);
+ expect(err.recommendations).toContain('Verify the banking server URL is correct');
+ done();
+ });
+ });
+
+ it('should classify connection errors correctly', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'ECONNREFUSED', message: 'Connection refused' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.ConnectionError);
+ expect(err).toBeInstanceOf(Banking.NetworkError);
+ expect(err.code).toBe('CONNECTION_ERROR');
+ expect(err.retryable).toBe(true);
+ expect(err.maxRetries).toBe(3);
+ expect(err.recommendations).toContain('Check network connectivity');
+ done();
+ });
+ });
+
+ it('should classify certificate errors correctly', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').replyWithError({ code: 'CERT_HAS_EXPIRED', message: 'Certificate has expired' });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.CertificateError);
+ expect(err).toBeInstanceOf(Banking.NetworkError);
+ expect(err.code).toBe('CERTIFICATE_ERROR');
+ expect(err.retryable).toBe(false);
+ expect(err.recommendations).toContain('Verify SSL certificate configuration');
+ done();
+ });
+ });
+ });
+
+ describe('HTTP Status Code Classification', () => {
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ it('should classify 401 as InvalidCredentialsError', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(401, 'Unauthorized');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.InvalidCredentialsError);
+ expect(err).toBeInstanceOf(Banking.AuthenticationError);
+ expect(err.technicalDetails.httpStatus).toBe(401);
+ expect(err.retryable).toBe(false);
+ expect(err.recommendations).toContain('Verify username and password are correct');
+ done();
+ });
+ });
+
+ it('should classify 404 as AccountNotFoundError', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(404, 'Not Found');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.AccountNotFoundError);
+ expect(err).toBeInstanceOf(Banking.BankingBusinessError);
+ expect(err.technicalDetails.httpStatus).toBe(404);
+ expect(err.recommendations).toContain('Verify the account ID/number is correct');
+ done();
+ });
+ });
+
+ it('should classify 429 as TooManyRequestsError', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(429, 'Too Many Requests');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.TooManyRequestsError);
+ expect(err).toBeInstanceOf(Banking.RateLimitError);
+ expect(err.technicalDetails.httpStatus).toBe(429);
+ expect(err.retryable).toBe(true);
+ expect(err.retryAfter).toBe(300); // 5 minutes
+ expect(err.recommendations).toContain('Wait before making additional requests');
+ done();
+ });
+ });
+
+ it('should classify 503 as MaintenanceModeError', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(503, 'Service Unavailable');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.MaintenanceModeError);
+ expect(err).toBeInstanceOf(Banking.BankingBusinessError);
+ expect(err.technicalDetails.httpStatus).toBe(503);
+ expect(err.retryable).toBe(true);
+ expect(err.retryAfter).toBe(3600); // 1 hour
+ expect(err.recommendations).toContain('Wait for maintenance window to complete');
+ done();
+ });
+ });
+ });
+
+ describe('Date Validation', () => {
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ it('should validate missing start date', () => {
+ return new Promise(resolve => {
+ banking.getStatement({}, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.InvalidDateRangeError);
+ expect(err.code).toBe('INVALID_DATE_RANGE');
+ expect(err.message).toContain('Start date is required');
+ resolve();
+ });
+ });
+ });
+
+ it('should validate invalid date format', () => {
+ return new Promise(resolve => {
+ banking.getStatement({ start: 'invalid-date' }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.InvalidDateRangeError);
+ expect(err.message).toContain('Start date must be in YYYYMMDD or YYYYMMDDHHMMSS format');
+ resolve();
+ });
+ });
+ });
+
+ it('should validate date range order', () => {
+ return new Promise(resolve => {
+ banking.getStatement({ start: 20241201, end: 20241101 }, (err, res) => {
+ expect(err).toBeInstanceOf(Banking.InvalidDateRangeError);
+ expect(err.message).toContain('Start date must be before end date');
+ resolve();
+ });
+ });
+ });
+
+ it('should accept valid date formats', done => {
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+0
+INFO
+
+20241101000000
+ENG
+
+Wells Fargo
+4000
+
+
+
+
+
+1
+
+0
+INFO
+
+
+USD
+
+121000248
+123456789
+CHECKING
+
+
+20241101
+20241201
+
+
+1000.00
+20241201
+
+
+
+
+ `
+ );
+
+ // Test YYYYMMDD format
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeFalsy();
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+ });
+
+ describe('Error Factory Function', () => {
+ it('should create appropriate error types from factory', () => {
+ const dnsError = Banking.createBankingError({
+ message: 'DNS lookup failed',
+ originalError: { code: 'ENOTFOUND' }
+ });
+ expect(dnsError).toBeInstanceOf(Banking.DNSError);
+
+ const authError = Banking.createBankingError({
+ message: 'Invalid credentials',
+ httpStatus: 401
+ });
+ expect(authError).toBeInstanceOf(Banking.InvalidCredentialsError);
+
+ const rateError = Banking.createBankingError({
+ message: 'Rate limited',
+ code: 'RATE_LIMITED'
+ });
+ expect(rateError).toBeInstanceOf(Banking.RateLimitError);
+ });
+
+ it('should preserve banking context in factory-created errors', () => {
+ const error = Banking.createBankingError(
+ {
+ message: 'Test error',
+ httpStatus: 500
+ },
+ {
+ fid: 12345,
+ operationType: 'statement',
+ url: 'https://bank.com/ofx'
+ }
+ );
+
+ expect(error.bankingContext.fid).toBe(12345);
+ expect(error.bankingContext.operationType).toBe('statement');
+ expect(error.bankingContext.url).toBe('https://bank.com/ofx');
+ });
+ });
+
+ describe('Error Serialization', () => {
+ it('should serialize to JSON correctly', () => {
+ const error = new Banking.TimeoutError('Connection timeout', {
+ fid: 12345,
+ operationType: 'statement',
+ metadata: { timeoutValue: 30000 }
+ });
+
+ const json = JSON.stringify(error);
+ const parsed = JSON.parse(json);
+
+ expect(parsed.name).toBe('TimeoutError');
+ expect(parsed.code).toBe('TIMEOUT_ERROR');
+ expect(parsed.category).toBe('NETWORK');
+ expect(parsed.retryable).toBe(true);
+ expect(parsed.bankingContext.fid).toBe(12345);
+ expect(parsed.bankingContext.operationType).toBe('statement');
+ });
+
+ it('should create log objects without sensitive data', () => {
+ const error = new Banking.BankingError('Test error', {
+ url: 'https://user:pass@bank.com/ofx?token=secret',
+ metadata: { sensitiveData: 'should not appear in logs' }
+ });
+
+ const logObj = error.toLogObject();
+
+ expect(logObj.bankingContext.url).not.toContain('user');
+ expect(logObj.bankingContext.url).not.toContain('pass');
+ expect(logObj.bankingContext.url).not.toContain('token');
+ expect(logObj.bankingContext.url).not.toContain('secret');
+ });
+ });
+
+ describe('Retry Recommendations', () => {
+ it('should provide correct retry information for different error types', () => {
+ const timeoutError = new Banking.TimeoutError('Timeout occurred');
+ expect(timeoutError.retryable).toBe(true);
+ expect(timeoutError.maxRetries).toBe(2);
+
+ const dnsError = new Banking.DNSError('DNS failed');
+ expect(dnsError.retryable).toBe(false);
+ expect(dnsError.maxRetries).toBe(0);
+
+ const authError = new Banking.InvalidCredentialsError('Bad credentials');
+ expect(authError.retryable).toBe(false);
+
+ const maintenanceError = new Banking.MaintenanceModeError('Under maintenance');
+ expect(maintenanceError.retryable).toBe(true);
+ expect(maintenanceError.retryAfter).toBe(3600);
+ });
+
+ it('should include actionable recommendations', () => {
+ const connectionError = new Banking.ConnectionError('Connection failed');
+ expect(connectionError.recommendations).toContain('Check network connectivity');
+ expect(connectionError.recommendations).toContain('Verify firewall settings');
+
+ const credentialsError = new Banking.InvalidCredentialsError('Invalid login');
+ expect(credentialsError.recommendations).toContain('Verify username and password are correct');
+ expect(credentialsError.recommendations).toContain('Check if account is locked or suspended');
+ });
+ });
+});
diff --git a/test/integration/timeout-retry.test.js b/test/integration/timeout-retry.test.js
new file mode 100644
index 0000000..3f2e25e
--- /dev/null
+++ b/test/integration/timeout-retry.test.js
@@ -0,0 +1,734 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import { bankConfigs } from '../fixtures/responses.js';
+
+describe('Comprehensive Timeout and Retry Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+
+ // Configure aggressive timeouts for testing
+ Banking.configurePool({
+ timeouts: {
+ quick: {
+ connection: 1000, // 1 second
+ request: 2000, // 2 seconds
+ socket: 1500, // 1.5 seconds
+ idle: 5000
+ },
+ standard: {
+ connection: 2000, // 2 seconds
+ request: 5000, // 5 seconds
+ socket: 3000, // 3 seconds
+ idle: 10000
+ },
+ heavy: {
+ connection: 3000, // 3 seconds
+ request: 10000, // 10 seconds
+ socket: 5000, // 5 seconds
+ idle: 15000
+ }
+ },
+ retry: {
+ maxRetries: {
+ quick: 2,
+ standard: 3,
+ heavy: 2
+ },
+ baseDelay: 100, // Fast retries for testing
+ maxDelay: 2000,
+ backoffStrategy: 'exponential',
+ jitter: {
+ enabled: true,
+ type: 'equal',
+ factor: 0.1
+ }
+ }
+ });
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ Banking.destroyPool();
+ });
+
+ describe('Operation Type Classification', () => {
+ it('should classify account list requests as quick operations', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(1800) // Just under quick timeout limit
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ account-list-success
+
+ 0
+ INFO
+
+
+ 20241201
+
+ Checking Account
+ 555-1234
+
+ 123456789
+ 987654321
+ CHECKING
+
+
+
+
+
+ `
+ );
+
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(elapsed).toBeGreaterThan(1800);
+ expect(elapsed).toBeLessThan(2500); // Should complete before standard timeout
+ done();
+ });
+ });
+
+ it('should classify 30-day statement requests as quick operations', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(1800)
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ quick-statement
+
+ 0
+ INFO
+
+
+ USD
+
+ 123456789
+ 987654321
+ CHECKING
+
+
+ 20241101
+ 20241201
+
+ DEBIT
+ 20241115
+ -50.00
+ quick123
+ Quick Transaction
+
+
+
+
+
+ `
+ );
+
+ // Request 30 days (should be classified as quick)
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(elapsed).toBeGreaterThan(1800);
+ expect(elapsed).toBeLessThan(2500);
+ done();
+ });
+ });
+
+ it('should classify 6-month statement requests as standard operations', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(4000) // Under standard timeout
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ standard-statement
+
+ 0
+ INFO
+
+
+ USD
+
+ 123456789
+ 987654321
+ CHECKING
+
+
+ 20240601
+ 20241201
+
+
+
+
+ `
+ );
+
+ // Request 6 months (should be classified as standard)
+ banking.getStatement({ start: 20240601, end: 20241201 }, (err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(elapsed).toBeGreaterThan(4000);
+ expect(elapsed).toBeLessThan(6000);
+ done();
+ });
+ });
+
+ it('should classify 2-year statement requests as heavy operations', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(8000) // Under heavy timeout
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ heavy-statement
+
+ 0
+ INFO
+
+
+ USD
+
+ 123456789
+ 987654321
+ CHECKING
+
+
+ 20221201
+ 20241201
+
+
+
+
+ `
+ );
+
+ // Request 2 years (should be classified as heavy)
+ banking.getStatement({ start: 20221201, end: 20241201 }, (err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(elapsed).toBeGreaterThan(8000);
+ expect(elapsed).toBeLessThan(11000);
+ done();
+ });
+ });
+ });
+
+ describe('Timeout Handling', () => {
+ it('should timeout quick operations appropriately', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(3000) // Longer than quick timeout
+ .reply(200, 'Should not reach here');
+
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toMatch(/ETIMEDOUT|ESOCKETTIMEDOUT|ECONNTIMEDOUT/);
+ expect(elapsed).toBeGreaterThan(2000);
+ expect(elapsed).toBeLessThan(4000);
+ done();
+ });
+ });
+
+ it('should timeout standard operations appropriately', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(7000) // Longer than standard timeout
+ .reply(200, 'Should not reach here');
+
+ banking.getStatement({ start: 20240601, end: 20241201 }, (err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toMatch(/ETIMEDOUT|ESOCKETTIMEDOUT|ECONNTIMEDOUT/);
+ expect(elapsed).toBeGreaterThan(5000);
+ expect(elapsed).toBeLessThan(8000);
+ done();
+ });
+ });
+
+ it('should timeout heavy operations appropriately', done => {
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(12000) // Longer than heavy timeout
+ .reply(200, 'Should not reach here');
+
+ banking.getStatement({ start: 20221201, end: 20241201 }, (err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toMatch(/ETIMEDOUT|ESOCKETTIMEDOUT|ECONNTIMEDOUT/);
+ expect(elapsed).toBeGreaterThan(10000);
+ expect(elapsed).toBeLessThan(15000);
+ done();
+ });
+ }, 20000); // Increase test timeout
+ });
+
+ describe('Retry Logic', () => {
+ it('should retry on connection reset errors', done => {
+ let attemptCount = 0;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').times(2).replyWithError({ code: 'ECONNRESET', message: 'Connection reset by peer' });
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+ `
+ );
+
+ const startTime = Date.now();
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(elapsed).toBeGreaterThan(200); // Should have some retry delay
+ done();
+ });
+ });
+
+ it('should retry on 500 server errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').times(2).reply(500, 'Internal Server Error');
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+ `
+ );
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should NOT retry on 401 authentication errors', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(401, 'Unauthorized - Invalid Credentials');
+
+ const startTime = Date.now();
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.statusCode).toBe(401);
+ expect(elapsed).toBeLessThan(1000); // Should fail quickly without retries
+ done();
+ });
+ });
+
+ it('should NOT retry on OFX invalid credentials errors', done => {
+ const invalidCredentialsResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 15500
+ ERROR
+ INVALID SIGNON
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, invalidCredentialsResponse);
+
+ const startTime = Date.now();
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBe(false); // OFX errors are not HTTP errors
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(elapsed).toBeLessThan(1000); // Should complete quickly
+ done();
+ });
+ });
+
+ it('should respect maximum retry limits', done => {
+ // Mock 4 failures (more than max retries for quick operations)
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .times(3) // 2 retries + 1 initial = 3 total attempts
+ .replyWithError({ code: 'ECONNRESET', message: 'Connection reset by peer' });
+
+ const startTime = Date.now();
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ECONNRESET');
+ expect(elapsed).toBeGreaterThan(200); // Should have some retry delays
+ done();
+ });
+ });
+ });
+
+ describe('Exponential Backoff', () => {
+ it('should increase delay between retries exponentially', done => {
+ const delays = [];
+ let startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').times(3).replyWithError({ code: 'ETIMEDOUT', message: 'Request timeout' });
+
+ banking.getAccounts((err, res) => {
+ const totalTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ETIMEDOUT');
+ // With exponential backoff (base 100ms): attempt 1 -> wait ~100ms -> attempt 2 -> wait ~200ms -> attempt 3
+ expect(totalTime).toBeGreaterThan(300); // At least base delays
+ done();
+ });
+ });
+ });
+
+ describe('Rate Limiting', () => {
+ it('should enforce rate limiting between requests', done => {
+ // Configure more aggressive rate limiting for testing
+ Banking.configurePool({
+ retry: {
+ rateLimiting: {
+ enabled: true,
+ maxConcurrent: 1,
+ requestInterval: 200 // 200ms between requests
+ }
+ }
+ });
+
+ const startTime = Date.now();
+ let firstCompleted = false;
+ let firstTime = 0;
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .times(2)
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+ `
+ );
+
+ // Make first request
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ firstCompleted = true;
+ firstTime = Date.now() - startTime;
+ });
+
+ // Make second request immediately
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(firstCompleted).toBe(true);
+
+ const secondTime = Date.now() - startTime;
+ const timeDiff = secondTime - firstTime;
+
+ // Second request should be delayed by rate limiting
+ expect(timeDiff).toBeGreaterThan(200);
+ done();
+ });
+ });
+ });
+
+ describe('Metrics Collection', () => {
+ it('should collect comprehensive metrics', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(500, 'Server Error');
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .reply(
+ 200,
+ `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+ `
+ );
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+
+ const metrics = Banking.getPoolMetrics();
+ expect(metrics).toBeDefined();
+ expect(metrics.totalRequests).toBeGreaterThan(0);
+ expect(metrics.operationTypes.quick).toBeGreaterThan(0);
+
+ const retryMetrics = Banking.getRetryMetrics();
+ expect(retryMetrics).toBeDefined();
+ expect(retryMetrics.httpErrors).toBeGreaterThan(0);
+ expect(retryMetrics.successfulRetries).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+
+ it('should reset retry metrics when requested', () => {
+ const initialMetrics = Banking.getRetryMetrics();
+ Banking.resetRetryMetrics();
+ const resetMetrics = Banking.getRetryMetrics();
+
+ expect(resetMetrics.totalAttempts).toBe(0);
+ expect(resetMetrics.successfulRetries).toBe(0);
+ expect(resetMetrics.failedRetries).toBe(0);
+ });
+ });
+
+ describe('Configuration Flexibility', () => {
+ it('should allow custom timeout configuration', done => {
+ Banking.configureTimeouts({
+ quick: {
+ connection: 500,
+ request: 1000,
+ socket: 750
+ }
+ });
+
+ const startTime = Date.now();
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .delay(1200) // Longer than custom quick timeout
+ .reply(200, 'Should not reach here');
+
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toMatch(/ETIMEDOUT|ESOCKETTIMEDOUT|ECONNTIMEDOUT/);
+ expect(elapsed).toBeGreaterThan(1000);
+ expect(elapsed).toBeLessThan(1500);
+ done();
+ });
+ });
+
+ it('should allow custom retry configuration', done => {
+ Banking.configureRetry({
+ maxRetries: {
+ quick: 1 // Only 1 retry
+ },
+ baseDelay: 50,
+ backoffStrategy: 'fixed'
+ });
+
+ nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp')
+ .times(2) // Should only make 2 attempts total (1 retry)
+ .replyWithError({ code: 'ECONNRESET', message: 'Connection reset' });
+
+ const startTime = Date.now();
+ banking.getAccounts((err, res) => {
+ const elapsed = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ECONNRESET');
+ expect(elapsed).toBeGreaterThan(50); // At least one retry delay
+ expect(elapsed).toBeLessThan(200); // But not too many retries
+ done();
+ });
+ });
+ });
+});
diff --git a/test/integration/us-bank.test.js b/test/integration/us-bank.test.js
new file mode 100644
index 0000000..764cd72
--- /dev/null
+++ b/test/integration/us-bank.test.js
@@ -0,0 +1,538 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import {
+ usBankStatementResponse,
+ invalidCredentialsResponse,
+ accountNotFoundResponse,
+ malformedOFXResponse,
+ bankConfigs
+} from '../fixtures/responses.js';
+
+describe('US Bank Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.usBank);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve US Bank statement', done => {
+ // Mock the OFX server response
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, usBankStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.ORG).toBe('U.S. Bank');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.FID).toBe('1001');
+
+ // Verify bank statement data
+ expect(ofx.BANKMSGSRSV1).toBeDefined();
+ const stmtrs = ofx.BANKMSGSRSV1.STMTTRNRS.STMTRS;
+ expect(stmtrs.CURDEF).toBe('USD');
+ expect(stmtrs.BANKACCTFROM.BANKID).toBe('091000022');
+ expect(stmtrs.BANKACCTFROM.ACCTID).toBe('7777888899');
+ expect(stmtrs.BANKACCTFROM.ACCTTYPE).toBe('SAVINGS');
+
+ // Verify transactions
+ const transactions = stmtrs.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBeGreaterThan(0);
+
+ // Verify specific US Bank transactions
+ const transferTransaction = transactions.find(t => t.NAME === 'TRANSFER FROM CHECKING');
+ expect(transferTransaction).toBeDefined();
+ expect(transferTransaction.TRNTYPE).toBe('CREDIT');
+ expect(transferTransaction.TRNAMT).toBe('5000.00');
+ expect(transferTransaction.MEMO).toBe('INTERNAL TRANSFER');
+
+ const interestTransaction = transactions.find(t => t.NAME === 'INTEREST PAYMENT');
+ expect(interestTransaction).toBeDefined();
+ expect(interestTransaction.TRNTYPE).toBe('CREDIT');
+ expect(interestTransaction.TRNAMT).toBe('15.25');
+ expect(interestTransaction.MEMO).toBe('MONTHLY INTEREST');
+
+ // Verify balance information
+ expect(stmtrs.LEDGERBAL).toBeDefined();
+ expect(stmtrs.LEDGERBAL.BALAMT).toBe('15015.25');
+ expect(stmtrs.AVAILBAL).toBeDefined();
+ expect(stmtrs.AVAILBAL.BALAMT).toBe('15015.25');
+
+ done();
+ });
+ });
+
+ it('should handle US Bank authentication errors', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('INVALID SIGNON');
+ done();
+ });
+ });
+
+ it('should send properly formatted US Bank OFX request', done => {
+ const scope = nock('https://www.usbank.com')
+ .post('/ofxroot', body => {
+ // Verify US Bank-specific OFX request format
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('DATA:OFXSGML');
+ expect(body).toContain('VERSION:102');
+ expect(body).toContain('testuser');
+ expect(body).toContain('testpass');
+ expect(body).toContain('1001');
+ expect(body).toContain('091000022');
+ expect(body).toContain('7777888899');
+ expect(body).toContain('SAVINGS');
+ expect(body).toContain('20241101');
+ expect(body).toContain('20241201');
+ return true;
+ })
+ .reply(200, usBankStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should handle US Bank server errors', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(500, 'Internal Server Error');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle US Bank connection timeouts', done => {
+ nock('https://www.usbank.com').post('/ofxroot').delay(8000).reply(200, usBankStatementResponse);
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(elapsedTime).toBeGreaterThan(1000);
+ done();
+ });
+ });
+
+ it('should handle US Bank account not found error', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, accountNotFoundResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10500');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID ACCOUNT NUMBER');
+ done();
+ });
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should successfully retrieve US Bank account list', done => {
+ const usBankAccountListResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:test-uid-usbank-accounts
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000[-6:CST]
+ ENG
+
+ U.S. Bank
+ 1001
+
+
+
+
+
+ usbank-accounts-uid-202
+
+ 0
+ INFO
+ SUCCESS
+
+
+ 20241201120000.000
+
+
+
+ 091000022
+ 7777888899
+ SAVINGS
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 091000022
+ 1111222233
+ CHECKING
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ 091000022
+ 9999888877
+ MONEYMRKT
+
+ Y
+ Y
+ Y
+ ACTIVE
+
+
+
+
+
+ `;
+
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, usBankAccountListResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ const acctInfo = res.body.OFX.SIGNUPMSGSRSV1.ACCTINFOTRNRS.ACCTINFORS.ACCTINFO;
+ expect(Array.isArray(acctInfo)).toBe(true);
+ expect(acctInfo.length).toBe(3); // Savings, checking, and money market
+
+ // Verify savings account
+ const savingsAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'SAVINGS');
+ expect(savingsAccount).toBeDefined();
+ expect(savingsAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('7777888899');
+ expect(savingsAccount.BANKACCTINFO.BANKACCTFROM.BANKID).toBe('091000022');
+ expect(savingsAccount.BANKACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ // Verify checking account
+ const checkingAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'CHECKING');
+ expect(checkingAccount).toBeDefined();
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('1111222233');
+
+ // Verify money market account
+ const moneyMarketAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'MONEYMRKT');
+ expect(moneyMarketAccount).toBeDefined();
+ expect(moneyMarketAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('9999888877');
+ expect(moneyMarketAccount.BANKACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ done();
+ });
+ });
+ });
+
+ describe('US Bank Specific Features', () => {
+ it('should handle US Bank interest calculations correctly', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, usBankStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+
+ // Verify interest transaction handling
+ const interestTransactions = transactions.filter(t => t.NAME === 'INTEREST PAYMENT' || t.MEMO === 'MONTHLY INTEREST');
+ expect(interestTransactions.length).toBeGreaterThan(0);
+
+ const interestTransaction = interestTransactions[0];
+ expect(interestTransaction.TRNTYPE).toBe('CREDIT');
+ expect(parseFloat(interestTransaction.TRNAMT)).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+
+ it('should handle US Bank internal transfers', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, usBankStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+
+ const transactions = res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.STMTTRN;
+
+ // Verify internal transfer transaction
+ const transferTransactions = transactions.filter(t => t.MEMO === 'INTERNAL TRANSFER' || t.NAME.includes('TRANSFER'));
+ expect(transferTransactions.length).toBeGreaterThan(0);
+
+ const transferTransaction = transferTransactions[0];
+ expect(transferTransaction.NAME).toBe('TRANSFER FROM CHECKING');
+ expect(transferTransaction.TRNTYPE).toBe('CREDIT');
+ expect(parseFloat(transferTransaction.TRNAMT)).toBeGreaterThan(0);
+
+ done();
+ });
+ });
+
+ it('should handle US Bank routing number validation', done => {
+ const invalidRoutingBanking = new Banking({
+ ...bankConfigs.usBank,
+ bankId: '999999999' // Invalid routing number
+ });
+
+ const invalidRoutingResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ invalid-routing-error
+
+ 10400
+ ERROR
+ INVALID BANK ROUTING NUMBER
+
+
+
+ `;
+
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, invalidRoutingResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ invalidRoutingBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10400');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID BANK ROUTING NUMBER');
+ done();
+ });
+ });
+
+ it('should handle US Bank account closure scenarios', done => {
+ const closedAccountResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20241201120000.000
+ ENG
+
+
+
+
+ closed-account-error
+
+ 10404
+ ERROR
+ ACCOUNT CLOSED
+
+
+
+ `;
+
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, closedAccountResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10404');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('ACCOUNT CLOSED');
+ done();
+ });
+ });
+
+ it('should handle different savings account types', done => {
+ // Test with a different savings account configuration
+ const premiumSavingsBanking = new Banking({
+ ...bankConfigs.usBank,
+ accId: '5555666677',
+ accType: 'SAVINGS'
+ });
+
+ const premiumSavingsResponse = usBankStatementResponse.replace('7777888899', '5555666677');
+
+ const scope = nock('https://www.usbank.com')
+ .post('/ofxroot', body => {
+ expect(body).toContain('5555666677');
+ expect(body).toContain('SAVINGS');
+ return true;
+ })
+ .reply(200, premiumSavingsResponse);
+
+ premiumSavingsBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKACCTFROM.ACCTID).toBe('5555666677');
+ done();
+ });
+ });
+ });
+
+ describe('US Bank Error Scenarios', () => {
+ it('should handle US Bank maintenance windows', done => {
+ const maintenanceResponse = `OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 2025
+ ERROR
+ SYSTEM UNAVAILABLE - MAINTENANCE
+
+ 20241201120000.000
+ ENG
+
+
+ `;
+
+ nock('https://www.usbank.com').post('/ofxroot').reply(503, maintenanceResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle malformed OFX responses', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, malformedOFXResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle network connectivity issues', done => {
+ nock('https://www.usbank.com').post('/ofxroot').replyWithError('ENETUNREACH');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ expect(err.code).toBe('ENETUNREACH');
+ done();
+ });
+ });
+
+ it('should handle US Bank rate limiting', done => {
+ nock('https://www.usbank.com').post('/ofxroot').reply(429, 'Too Many Requests', {
+ 'Retry-After': '60'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('Date and Transaction Handling', () => {
+ it('should handle empty transaction periods', done => {
+ const emptyTransactionResponse = usBankStatementResponse.replace(/[\s\S]*?<\/STMTTRN>/g, '');
+
+ nock('https://www.usbank.com').post('/ofxroot').reply(200, emptyTransactionResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241102 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST).toBeDefined();
+ done();
+ });
+ });
+
+ it('should validate date range parameters', done => {
+ const scope = nock('https://www.usbank.com')
+ .post('/ofxroot', body => {
+ expect(body).toContain('20241101');
+ expect(body).toContain('20241201');
+ return true;
+ })
+ .reply(200, usBankStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/integration/wells-fargo.test.js b/test/integration/wells-fargo.test.js
new file mode 100644
index 0000000..5f6c065
--- /dev/null
+++ b/test/integration/wells-fargo.test.js
@@ -0,0 +1,277 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import nock from 'nock';
+import Banking from '../../index.js';
+import {
+ wellsFargoStatementResponse,
+ wellsFargoAccountListResponse,
+ invalidCredentialsResponse,
+ accountNotFoundResponse,
+ malformedOFXResponse,
+ bankConfigs
+} from '../fixtures/responses.js';
+
+describe('Wells Fargo Integration Tests', () => {
+ let banking;
+
+ beforeEach(() => {
+ banking = new Banking(bankConfigs.wellsFargo);
+ });
+
+ describe('getStatement', () => {
+ it('should successfully retrieve transaction statement', done => {
+ // Mock the OFX server response
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoStatementResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.ORG).toBe('Wells Fargo');
+ expect(ofx.SIGNONMSGSRSV1.SONRS.FI.FID).toBe('3001');
+
+ // Verify bank statement data
+ expect(ofx.BANKMSGSRSV1).toBeDefined();
+ const stmtrs = ofx.BANKMSGSRSV1.STMTTRNRS.STMTRS;
+ expect(stmtrs.CURDEF).toBe('USD');
+ expect(stmtrs.BANKACCTFROM.BANKID).toBe('123006800');
+ expect(stmtrs.BANKACCTFROM.ACCTID).toBe('1234567890');
+ expect(stmtrs.BANKACCTFROM.ACCTTYPE).toBe('CHECKING');
+
+ // Verify transactions
+ const transactions = stmtrs.BANKTRANLIST.STMTTRN;
+ expect(Array.isArray(transactions)).toBe(true);
+ expect(transactions.length).toBeGreaterThan(0);
+
+ // Verify specific transaction data
+ const debitTransaction = transactions.find(t => t.TRNTYPE === 'DEBIT');
+ expect(debitTransaction).toBeDefined();
+ expect(debitTransaction.TRNAMT).toBe('-150.00');
+ expect(debitTransaction.NAME).toBe('GROCERY STORE PURCHASE');
+
+ const creditTransaction = transactions.find(t => t.TRNTYPE === 'CREDIT');
+ expect(creditTransaction).toBeDefined();
+ expect(creditTransaction.TRNAMT).toBe('2500.00');
+ expect(creditTransaction.NAME).toBe('DIRECT DEPOSIT');
+
+ // Verify balance information
+ expect(stmtrs.LEDGERBAL).toBeDefined();
+ expect(stmtrs.LEDGERBAL.BALAMT).toBe('2338.00');
+ expect(stmtrs.AVAILBAL).toBeDefined();
+ expect(stmtrs.AVAILBAL.BALAMT).toBe('2338.00');
+
+ done();
+ });
+ });
+
+ it('should handle invalid credentials error', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.MESSAGE).toBe('INVALID SIGNON');
+ done();
+ });
+ });
+
+ it('should handle account not found error', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, accountNotFoundResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.CODE).toBe('10500');
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STATUS.MESSAGE).toBe('INVALID ACCOUNT NUMBER');
+ done();
+ });
+ });
+
+ it('should handle network timeout', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').delay(5000).reply(200, wellsFargoStatementResponse);
+
+ const startTime = Date.now();
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ const elapsedTime = Date.now() - startTime;
+ expect(err).toBeTruthy();
+ expect(elapsedTime).toBeGreaterThan(1000); // Should timeout
+ done();
+ });
+ });
+
+ it('should handle malformed OFX response', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, malformedOFXResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ // Should handle parsing errors gracefully
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should handle HTTP 500 server error', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(500, 'Internal Server Error');
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should successfully retrieve account list', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoAccountListResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body).toBeDefined();
+ expect(res.body.OFX).toBeDefined();
+
+ // Verify OFX structure
+ const ofx = res.body.OFX;
+ expect(ofx.SIGNONMSGSRSV1).toBeDefined();
+ expect(ofx.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('0');
+
+ // Verify account information
+ expect(ofx.SIGNUPMSGSRSV1).toBeDefined();
+ const acctInfo = ofx.SIGNUPMSGSRSV1.ACCTINFOTRNRS.ACCTINFORS.ACCTINFO;
+ expect(Array.isArray(acctInfo)).toBe(true);
+ expect(acctInfo.length).toBeGreaterThan(0);
+
+ // Verify checking account
+ const checkingAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'CHECKING');
+ expect(checkingAccount).toBeDefined();
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('1234567890');
+ expect(checkingAccount.BANKACCTINFO.BANKACCTFROM.BANKID).toBe('123006800');
+ expect(checkingAccount.BANKACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ // Verify savings account
+ const savingsAccount = acctInfo.find(acc => acc.BANKACCTINFO && acc.BANKACCTINFO.BANKACCTFROM.ACCTTYPE === 'SAVINGS');
+ expect(savingsAccount).toBeDefined();
+ expect(savingsAccount.BANKACCTINFO.BANKACCTFROM.ACCTID).toBe('9876543210');
+ expect(savingsAccount.BANKACCTINFO.SVCSTATUS).toBe('ACTIVE');
+
+ done();
+ });
+ });
+
+ it('should handle authentication error for account list', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, invalidCredentialsResponse, {
+ 'Content-Type': 'application/x-ofx'
+ });
+
+ banking.getAccounts((err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.SIGNONMSGSRSV1.SONRS.STATUS.CODE).toBe('15500');
+ done();
+ });
+ });
+ });
+
+ describe('OFX Request Validation', () => {
+ it('should send properly formatted OFX request for statement', done => {
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ // Verify OFX request format
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('DATA:OFXSGML');
+ expect(body).toContain('VERSION:');
+ expect(body).toContain('testuser');
+ expect(body).toContain('testpass');
+ expect(body).toContain('3001');
+ expect(body).toContain('123006800');
+ expect(body).toContain('1234567890');
+ expect(body).toContain('CHECKING');
+ expect(body).toContain('20241101');
+ expect(body).toContain('20241201');
+ return true;
+ })
+ .reply(200, wellsFargoStatementResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+
+ it('should send properly formatted OFX request for account list', done => {
+ const scope = nock('https://www.oasis.cfree.com')
+ .post('/3001.ofxgp', body => {
+ // Verify OFX account list request format
+ expect(body).toContain('OFXHEADER:100');
+ expect(body).toContain('testuser');
+ expect(body).toContain('testpass');
+ expect(body).toContain('3001');
+ expect(body).toContain('');
+ return true;
+ })
+ .reply(200, wellsFargoAccountListResponse);
+
+ banking.getAccounts((err, res) => {
+ expect(scope.isDone()).toBe(true);
+ expect(err).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle empty transaction list', done => {
+ const emptyTransactionResponse = wellsFargoStatementResponse.replace(/[\s\S]*?<\/STMTTRN>/g, '');
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, emptyTransactionResponse);
+
+ banking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ expect(res.body.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle different date ranges', done => {
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoStatementResponse);
+
+ banking.getStatement({ start: 20240101, end: 20241231 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+
+ it('should handle custom headers', done => {
+ const customBanking = new Banking({
+ ...bankConfigs.wellsFargo,
+ headers: ['Content-Type', 'Host', 'Content-Length', 'Connection']
+ });
+
+ nock('https://www.oasis.cfree.com').post('/3001.ofxgp').reply(200, wellsFargoStatementResponse);
+
+ customBanking.getStatement({ start: 20241101, end: 20241201 }, (err, res) => {
+ expect(err).toBe(false);
+ expect(res).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/test/parsing.js b/test/parsing.js
index 9915732..66f732b 100644
--- a/test/parsing.js
+++ b/test/parsing.js
@@ -1,13 +1,11 @@
-var Banking = require('..')
- , data = require('./fixtures/data')
- , mocha = require('mocha');
+const Banking = require('..'),
+ data = require('./fixtures/data'),
+ mocha = require('mocha');
-describe('Banking', function(){
-
- describe('banking.getStatement', function() {
- it('should return valid xml from the wells fargo server', function(done){
-
- var banking = Banking({
+describe('Banking', () => {
+ describe('banking.getStatement', () => {
+ it('should return valid xml from the wells fargo server', done => {
+ const banking = Banking({
fid: 3001,
fidOrg: 'Wells Fargo',
accType: 'checking',
@@ -19,17 +17,16 @@ describe('Banking', function(){
});
//If second param is omitted JSON will be returned by default
- banking.getStatement({start:20131101, end:20131120}, function (err, res) {
- if(err) done(res)
+ banking.getStatement({ start: 20131101, end: 20131120 }, (err, res) => {
+ if (err) done(res);
res.body.should.be.an.instanceof(Object);
res.body.should.have.property('OFX');
done();
});
});
- it('should return valid xml from the discovercard server', function(done){
-
- var banking = Banking({
+ it('should return valid xml from the discovercard server', done => {
+ const banking = Banking({
fid: 7101,
fidOrg: 'Discover Financial Services',
accType: 'checking',
@@ -42,8 +39,8 @@ describe('Banking', function(){
});
//If second param is omitted JSON will be returned by default
- banking.getStatement({start:20131101, end:20131120}, function (err, res) {
- if(err) done(res)
+ banking.getStatement({ start: 20131101, end: 20131120 }, (err, res) => {
+ if (err) done(res);
res.body.should.be.an.instanceof(Object);
res.body.should.have.property('OFX');
done();
@@ -51,16 +48,16 @@ describe('Banking', function(){
});
});
- describe('.version', function (){
- it('should output the current version', function (done){
+ describe('.version', () => {
+ it('should output the current version', done => {
Banking.version.should.equal(require('../package').version);
done();
});
- })
+ });
- describe('.parseFile', function(){
- it('should read the provided file and return JSON', function(done){
- Banking.parseFile(__dirname +'/fixtures/sample.ofx', function (res) {
+ describe('.parseFile', () => {
+ it('should read the provided file and return JSON', done => {
+ Banking.parseFile(`${__dirname}/fixtures/sample.ofx`, res => {
res.body.should.be.an.instanceof(Object);
res.body.should.have.property('OFX');
res.body.OFX.should.have.property('SIGNONMSGSRSV1');
@@ -70,8 +67,8 @@ describe('Banking', function(){
});
});
- it('should read a OFX file with end-tags in elements and return JSON', function(done){
- Banking.parseFile(__dirname +'/fixtures/sample-with-end-tags.ofx', function (res) {
+ it('should read a OFX file with end-tags in elements and return JSON', done => {
+ Banking.parseFile(`${__dirname}/fixtures/sample-with-end-tags.ofx`, res => {
res.body.should.be.an.instanceof(Object);
res.body.should.have.property('OFX');
res.body.should.have.property('OFX');
@@ -83,10 +80,9 @@ describe('Banking', function(){
});
});
- describe('.parse', function(){
- it('should read the provided string and return JSON', function(done){
-
- Banking.parse(data.ofxString, function (res) {
+ describe('.parse', () => {
+ it('should read the provided string and return JSON', done => {
+ Banking.parse(data.ofxString, res => {
res.body.should.be.an.instanceof(Object);
res.body.should.have.property('OFX');
done();
diff --git a/test/setup.js b/test/setup.js
new file mode 100644
index 0000000..224d6af
--- /dev/null
+++ b/test/setup.js
@@ -0,0 +1,33 @@
+import { beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
+import nock from 'nock';
+
+// Global test setup
+beforeAll(() => {
+ // Disable real HTTP requests during tests
+ nock.disableNetConnect();
+
+ // Allow localhost for local testing
+ nock.enableNetConnect('127.0.0.1');
+ nock.enableNetConnect('localhost');
+});
+
+beforeEach(() => {
+ // Clear any existing nock interceptors
+ nock.cleanAll();
+});
+
+afterEach(() => {
+ // Clean up nock interceptors after each test
+ nock.cleanAll();
+});
+
+afterAll(() => {
+ // Restore HTTP connections after all tests
+ nock.enableNetConnect();
+ nock.restore();
+});
+
+// Global error handler for unhandled rejections
+process.on('unhandledRejection', (reason, promise) => {
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
+});
diff --git a/test/test-runner.js b/test/test-runner.js
new file mode 100755
index 0000000..faba70f
--- /dev/null
+++ b/test/test-runner.js
@@ -0,0 +1,312 @@
+#!/usr/bin/env node
+/**
+ * Comprehensive Banking.js Test Runner
+ *
+ * This script runs all integration tests for the banking.js library,
+ * supporting both mock mode (default) and sandbox mode with real bank connections.
+ *
+ * Usage:
+ * npm run test:integration # Run all tests in mock mode
+ * node test/test-runner.js --sandbox # Run tests against sandbox environments
+ * node test/test-runner.js --bank wells # Run tests for specific bank only
+ * node test/test-runner.js --verbose # Run with detailed logging
+ * node test/test-runner.js --help # Show help
+ */
+
+import { spawn } from 'child_process';
+import { readFileSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
+
+// Configuration
+const config = {
+ timeout: 30000,
+ retries: 2,
+ parallel: true,
+ coverage: false,
+ ui: false,
+ watch: false,
+ sandbox: false,
+ verbose: false,
+ bank: null,
+ environment: 'test'
+};
+
+// Supported banks
+const supportedBanks = ['wells-fargo', 'discover', 'chase', 'bank-of-america', 'us-bank'];
+
+// Parse command line arguments
+function parseArgs() {
+ const args = process.argv.slice(2);
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+
+ switch (arg) {
+ case '--help':
+ case '-h':
+ showHelp();
+ process.exit(0);
+ break;
+
+ case '--sandbox':
+ config.sandbox = true;
+ console.log('šļø Running in SANDBOX mode - will attempt real bank connections');
+ break;
+
+ case '--mock':
+ config.sandbox = false;
+ console.log('š Running in MOCK mode - using mocked responses');
+ break;
+
+ case '--verbose':
+ case '-v':
+ config.verbose = true;
+ break;
+
+ case '--coverage':
+ config.coverage = true;
+ break;
+
+ case '--ui':
+ config.ui = true;
+ break;
+
+ case '--watch':
+ config.watch = true;
+ break;
+
+ case '--parallel':
+ config.parallel = true;
+ break;
+
+ case '--serial':
+ config.parallel = false;
+ break;
+
+ case '--bank':
+ if (i + 1 < args.length) {
+ const bankName = args[i + 1];
+ if (supportedBanks.includes(bankName)) {
+ config.bank = bankName;
+ i++; // Skip next argument
+ } else {
+ console.error(`ā Unknown bank: ${bankName}`);
+ console.log(`Supported banks: ${supportedBanks.join(', ')}`);
+ process.exit(1);
+ }
+ }
+ break;
+
+ case '--timeout':
+ if (i + 1 < args.length) {
+ config.timeout = parseInt(args[i + 1]);
+ i++;
+ }
+ break;
+
+ case '--retries':
+ if (i + 1 < args.length) {
+ config.retries = parseInt(args[i + 1]);
+ i++;
+ }
+ break;
+
+ default:
+ if (arg.startsWith('--')) {
+ console.warn(`ā ļø Unknown option: ${arg}`);
+ }
+ }
+ }
+}
+
+function showHelp() {
+ console.log(`
+Banking.js Integration Test Runner v${packageJson.version}
+
+USAGE:
+ npm run test:integration Run all integration tests in mock mode
+ node test/test-runner.js [OPTIONS] Run with specific options
+
+OPTIONS:
+ --help, -h Show this help message
+ --sandbox Run tests against sandbox/demo bank environments
+ --mock Run tests with mocked responses (default)
+ --verbose, -v Enable verbose logging
+ --coverage Generate coverage reports
+ --ui Open Vitest UI
+ --watch Watch mode for development
+ --parallel Run tests in parallel (default)
+ --serial Run tests serially
+ --bank Run tests for specific bank only
+ --timeout Set test timeout in milliseconds (default: 30000)
+ --retries Set number of retries for failed tests (default: 2)
+
+SUPPORTED BANKS:
+ ${supportedBanks.map(bank => ` ${bank}`).join('\n')}
+
+EXAMPLES:
+ node test/test-runner.js --bank wells-fargo --verbose
+ node test/test-runner.js --sandbox --coverage
+ node test/test-runner.js --watch --ui
+
+ENVIRONMENT VARIABLES:
+ NODE_ENV Set to 'test' for testing environment
+ DEBUG Enable debug logging for banking module
+ CI Continuous Integration mode (affects output format)
+
+TESTING MODES:
+ Mock Mode (default): Uses nock to mock HTTP responses, runs offline
+ Sandbox Mode: Attempts to connect to bank sandbox environments
+ (requires valid sandbox credentials)
+
+For more information, see: https://github.com/euforic/banking.js
+`);
+}
+
+function buildVitestArgs() {
+ const args = [];
+
+ // Basic command
+ if (config.watch) {
+ args.push('watch');
+ } else {
+ args.push('run');
+ }
+
+ // Test pattern
+ if (config.bank) {
+ args.push(`test/integration/${config.bank}.test.js`);
+ } else {
+ args.push('test/integration/');
+ }
+
+ // Coverage
+ if (config.coverage) {
+ args.push('--coverage');
+ }
+
+ // UI mode
+ if (config.ui) {
+ args.push('--ui');
+ }
+
+ // Parallel execution
+ if (!config.parallel) {
+ args.push('--pool', 'forks', '--poolOptions.forks.singleFork');
+ }
+
+ // Timeout
+ args.push('--testTimeout', config.timeout.toString());
+
+ // Retries
+ if (config.retries > 0) {
+ args.push('--retry', config.retries.toString());
+ }
+
+ // Reporter
+ if (process.env.CI) {
+ args.push('--reporter', 'json', '--reporter', 'verbose');
+ } else if (config.verbose) {
+ args.push('--reporter', 'verbose');
+ } else {
+ args.push('--reporter', 'default');
+ }
+
+ return args;
+}
+
+function setEnvironmentVariables() {
+ // Set test environment
+ process.env.NODE_ENV = config.environment;
+
+ // Set sandbox mode flag for tests to read
+ if (config.sandbox) {
+ process.env.BANKING_TEST_MODE = 'sandbox';
+ console.log('ā ļø WARNING: Sandbox mode requires valid bank credentials');
+ console.log(' Set environment variables for bank credentials if testing live connections');
+ } else {
+ process.env.BANKING_TEST_MODE = 'mock';
+ }
+
+ // Enable debug logging if verbose
+ if (config.verbose) {
+ process.env.DEBUG = 'banking:*';
+ }
+
+ // Set timeout for HTTP requests
+ process.env.BANKING_REQUEST_TIMEOUT = config.timeout.toString();
+}
+
+function runTests() {
+ return new Promise((resolve, reject) => {
+ console.log(`\nš¦ Banking.js Integration Test Suite v${packageJson.version}`);
+ console.log(`š Running ${config.bank || 'all banks'} tests in ${config.sandbox ? 'SANDBOX' : 'MOCK'} mode\n`);
+
+ const vitestArgs = buildVitestArgs();
+
+ if (config.verbose) {
+ console.log('š§ Vitest command:', 'npx vitest', vitestArgs.join(' '));
+ }
+
+ const child = spawn('npx', ['vitest', ...vitestArgs], {
+ stdio: 'inherit',
+ env: process.env
+ });
+
+ child.on('close', code => {
+ if (code === 0) {
+ console.log('\nā
All tests passed!');
+ if (config.sandbox) {
+ console.log('šļø Sandbox tests completed successfully');
+ }
+ resolve(code);
+ } else {
+ console.log(`\nā Tests failed with exit code ${code}`);
+ if (config.sandbox) {
+ console.log('šļø Note: Sandbox failures may be due to network or credential issues');
+ }
+ reject(new Error(`Tests failed with exit code ${code}`));
+ }
+ });
+
+ child.on('error', error => {
+ console.error('ā Failed to run tests:', error.message);
+ reject(error);
+ });
+ });
+}
+
+// Main execution
+async function main() {
+ try {
+ parseArgs();
+ setEnvironmentVariables();
+ await runTests();
+ process.exit(0);
+ } catch (error) {
+ console.error('ā Test runner failed:', error.message);
+ process.exit(1);
+ }
+}
+
+// Handle graceful shutdown
+process.on('SIGINT', () => {
+ console.log('\nā¹ļø Test runner interrupted');
+ process.exit(130);
+});
+
+process.on('SIGTERM', () => {
+ console.log('\nā¹ļø Test runner terminated');
+ process.exit(143);
+});
+
+// Only run if this file is executed directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main();
+}
+
+export { config, supportedBanks, buildVitestArgs };
diff --git a/vitest.config.js b/vitest.config.js
new file mode 100644
index 0000000..5de8294
--- /dev/null
+++ b/vitest.config.js
@@ -0,0 +1,36 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ // Test environment
+ environment: 'node',
+
+ // Global test settings
+ globals: true,
+
+ // Test file patterns
+ include: ['test/**/*.{test,spec}.{js,ts}'],
+ exclude: ['node_modules', 'dist'],
+
+ // Test timeout
+ testTimeout: 30000,
+
+ // Setup files
+ setupFiles: ['./test/setup.js'],
+
+ // Coverage configuration
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ exclude: ['node_modules/', 'test/', '*.config.js', 'index.d.ts']
+ },
+
+ // Concurrent testing
+ pool: 'threads',
+ poolOptions: {
+ threads: {
+ singleThread: false
+ }
+ }
+ }
+});