@naturalpay/sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -231
- package/dist/index.cjs +40 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +39 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/cli.cjs +1 -1
- package/dist/mcp/cli.cjs.map +1 -1
- package/dist/mcp/cli.js +1 -1
- package/dist/mcp/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/version.ts","../../src/logging.ts","../../src/mcp/cli.ts"],"names":["AsyncLocalStorage","Command","startStdioServer","ServerType"],"mappings":"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","file":"cli.cjs","sourcesContent":["/**\n * Single source of truth for the SDK version string.\n */\nexport const VERSION = '0.1.1';\n","/**\n * Structured logging for Natural Payments SDK.\n *\n * Supports two modes:\n * 1. Plain text logging (default, for local development)\n * 2. JSON logging with Datadog correlation (when NATURAL_LOG_FORMAT=json)\n *\n * Features:\n * - Source info: file, line, function on every log (for error grouping)\n * - Context binding for request_id, agent_id, instance_id\n * - Datadog trace correlation fields\n * - AsyncLocalStorage for proper async context isolation\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\nimport { VERSION } from './version.js';\n\n// Log levels (matching standard levels)\nexport type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nconst LOG_LEVEL_VALUES: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warning: 30,\n error: 40,\n};\n\n// AsyncLocalStorage for proper async context isolation\nconst asyncContext = new AsyncLocalStorage<Record<string, unknown>>();\n\n// Fallback global context for environments where AsyncLocalStorage isn't used\nlet globalContext: Record<string, unknown> = {};\n\n/**\n * Get the current logging context.\n * Uses AsyncLocalStorage if available, falls back to global context.\n */\nexport function getContext(): Record<string, unknown> {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n return { ...asyncStore };\n }\n return { ...globalContext };\n}\n\n/**\n * Sanitize a string for safe logging to prevent log injection.\n */\nfunction sanitizeString(value: string): string {\n let sanitized = '';\n for (const char of value) {\n if (char === '\\r' || char === '\\n') {\n sanitized += '\\\\n';\n } else if (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) {\n // Control characters - escape them\n sanitized += `\\\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`;\n } else {\n sanitized += char;\n }\n }\n // Truncate overly long values to prevent log flooding\n const maxLength = 1000;\n if (sanitized.length > maxLength) {\n sanitized = sanitized.slice(0, maxLength) + '...[truncated]';\n }\n return sanitized;\n}\n\n/**\n * Sanitize a value for safe logging to prevent log injection.\n *\n * Recursively sanitizes strings in objects and arrays.\n * Removes/escapes control characters, newlines, and other potentially\n * dangerous sequences that could be used for log injection attacks.\n */\nfunction sanitizeLogValue(value: unknown, depth = 0): unknown {\n // Prevent infinite recursion on deeply nested objects\n const maxDepth = 10;\n if (depth > maxDepth) {\n return '[max depth exceeded]';\n }\n\n if (value === null || value === undefined) {\n return value;\n }\n\n if (typeof value === 'string') {\n return sanitizeString(value);\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeLogValue(item, depth + 1));\n }\n\n if (typeof value === 'object') {\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const sanitizedKey = sanitizeString(key);\n sanitized[sanitizedKey] = sanitizeLogValue(val, depth + 1);\n }\n return sanitized;\n }\n\n // For functions, symbols, etc. - return type description\n return `[${typeof value}]`;\n}\n\n/**\n * Bind additional context to current scope (e.g., request_id, agent_id).\n *\n * Values are sanitized to prevent log injection attacks.\n * Updates both AsyncLocalStorage (if active) and global context.\n *\n * @example\n * bindContext({ requestId: 'req_123', agentId: 'agt_456' });\n * logger.info('Processing payment'); // Will include requestId and agentId\n */\nexport function bindContext(context: Record<string, unknown>): void {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n\n // Update AsyncLocalStorage if active\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n Object.assign(asyncStore, sanitized);\n }\n\n // Always update global context as fallback\n globalContext = { ...globalContext, ...sanitized };\n}\n\n/**\n * Clear all bound context.\n */\nexport function clearContext(): void {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n for (const key of Object.keys(asyncStore)) {\n delete asyncStore[key];\n }\n }\n globalContext = {};\n}\n\n/**\n * Run a function with isolated logging context.\n *\n * Context bound within the callback is isolated from other async operations.\n * This is the recommended way to set context for request handling.\n *\n * @example\n * await runWithContext({ requestId: 'req_123' }, async () => {\n * logger.info('Processing request'); // Includes requestId\n * await doAsyncWork();\n * logger.info('Request complete'); // Still includes requestId\n * });\n */\nexport function runWithContext<T>(\n context: Record<string, unknown>,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n return asyncContext.run(sanitized, fn);\n}\n\n// Logging configuration\ninterface LoggingConfig {\n level: LogLevel;\n jsonFormat: boolean;\n serviceName: string;\n environment: string;\n}\n\nconst loggingConfig: LoggingConfig = {\n level: (process.env['NATURAL_LOG_LEVEL']?.toLowerCase() as LogLevel) || 'info',\n jsonFormat: process.env['NATURAL_LOG_FORMAT']?.toLowerCase() === 'json',\n serviceName: 'naturalpay-sdk',\n environment: process.env['NATURAL_ENV'] || process.env['DD_ENV'] || 'development',\n};\n\n/**\n * Configure logging for the Natural SDK.\n *\n * @param options - Logging configuration options\n * @param options.level - Log level (debug, info, warning, error)\n * @param options.jsonFormat - Use JSON format (default: auto-detect from NATURAL_LOG_FORMAT env var)\n * @param options.serviceName - Service name for structured logs\n *\n * Environment variables:\n * - NATURAL_LOG_LEVEL: Override log level (DEBUG, INFO, WARNING, ERROR)\n * - NATURAL_LOG_FORMAT: Set to \"json\" for JSON output, \"text\" for plain text\n */\nexport function configureLogging(options?: {\n level?: LogLevel;\n jsonFormat?: boolean;\n serviceName?: string;\n}): void {\n if (options?.level) {\n loggingConfig.level = options.level;\n }\n if (options?.jsonFormat !== undefined) {\n loggingConfig.jsonFormat = options.jsonFormat;\n }\n if (options?.serviceName) {\n loggingConfig.serviceName = options.serviceName;\n }\n}\n\n/**\n * Get source location information from stack trace.\n */\nfunction getSourceInfo(): { file: string; line: number; function: string } {\n const stack = new Error().stack;\n if (!stack) {\n return { file: 'unknown', line: 0, function: 'unknown' };\n }\n\n // Parse stack trace - skip the first 3 lines (Error, getSourceInfo, formatLogRecord/log)\n const lines = stack.split('\\n');\n const callerLine = lines[4] || lines[3] || '';\n\n // Match patterns like \"at functionName (/path/to/file.ts:123:45)\" or \"at /path/to/file.ts:123:45\"\n const match = callerLine.match(/at\\s+(?:(.+?)\\s+\\()?(.*?):(\\d+):\\d+\\)?/);\n if (match) {\n return {\n function: match[1] || 'anonymous',\n file: match[2] || 'unknown',\n line: parseInt(match[3] || '0', 10),\n };\n }\n\n return { file: 'unknown', line: 0, function: 'unknown' };\n}\n\ninterface LogRecord {\n timestamp: string;\n level: string;\n logger: string;\n message: string;\n source: { file: string; line: number; function: string };\n service: string;\n environment: string;\n version: string;\n [key: string]: unknown;\n}\n\n/**\n * Format a log record as JSON.\n */\nfunction formatJsonLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const record: LogRecord = {\n timestamp: new Date().toISOString(),\n level: level.toUpperCase(),\n logger: loggerName,\n message,\n source: getSourceInfo(),\n service: loggingConfig.serviceName,\n environment: loggingConfig.environment,\n version: VERSION,\n ...getContext(),\n ...extra,\n };\n\n // Add Datadog trace correlation if available\n const ddTraceId = process.env['DD_TRACE_ID'];\n const ddSpanId = process.env['DD_SPAN_ID'];\n if (ddTraceId) {\n record['dd.trace_id'] = ddTraceId;\n record['dd.span_id'] = ddSpanId;\n record['dd.service'] = loggingConfig.serviceName;\n record['dd.version'] = VERSION;\n record['dd.env'] = loggingConfig.environment;\n }\n\n return JSON.stringify(record);\n}\n\n/**\n * Format a log record as plain text.\n */\nfunction formatTextLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const contextEntries = Object.entries({ ...getContext(), ...extra });\n const contextStr =\n contextEntries.length > 0 ? ` [${contextEntries.map(([k, v]) => `${k}=${v}`).join(', ')}]` : '';\n\n return `${level.toUpperCase().padEnd(8)} ${loggerName}: ${message}${contextStr}`;\n}\n\n/**\n * Logger instance for a specific module/context.\n */\nexport class Logger {\n constructor(private readonly name: string) {}\n\n private log(level: LogLevel, message: string, extra?: Record<string, unknown>): void {\n if (LOG_LEVEL_VALUES[level] < LOG_LEVEL_VALUES[loggingConfig.level]) {\n return;\n }\n\n const formatted = loggingConfig.jsonFormat\n ? formatJsonLog(level, this.name, message, extra)\n : formatTextLog(level, this.name, message, extra);\n\n // Use stderr for logs (best practice for CLI tools)\n if (level === 'error') {\n console.error(formatted);\n } else if (level === 'warning') {\n console.warn(formatted);\n } else {\n console.error(formatted); // All logs to stderr\n }\n }\n\n debug(message: string, extra?: Record<string, unknown>): void {\n this.log('debug', message, extra);\n }\n\n info(message: string, extra?: Record<string, unknown>): void {\n this.log('info', message, extra);\n }\n\n warning(message: string, extra?: Record<string, unknown>): void {\n this.log('warning', message, extra);\n }\n\n warn(message: string, extra?: Record<string, unknown>): void {\n this.warning(message, extra);\n }\n\n error(message: string, extra?: Record<string, unknown>): void {\n this.log('error', message, extra);\n }\n}\n\n/**\n * Get a logger for the given module name.\n *\n * @param name - Module name (e.g., 'naturalpay.http')\n * @returns Logger instance\n *\n * @example\n * const logger = getLogger('naturalpay.payments');\n * logger.info('Payment initiated', { amount: 10000 });\n */\nexport function getLogger(name: string): Logger {\n if (!name.startsWith('naturalpay')) {\n name = `naturalpay.${name}`;\n }\n return new Logger(name);\n}\n\n// Helper functions for common logging patterns\n\n/**\n * Log an error with full context and exception info.\n */\nexport function logError(\n logger: Logger,\n message: string,\n options?: {\n error?: Error;\n statusCode?: number;\n code?: string;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = { ...options };\n\n if (options?.error) {\n extra['errorType'] = options.error.name;\n extra['errorMessage'] = options.error.message;\n if (options.error.stack) {\n extra['errorStack'] = options.error.stack.split('\\n').slice(0, 5).join('\\n');\n }\n delete extra['error'];\n }\n\n logger.error(message, extra);\n}\n\n/**\n * Log an API call with standard fields.\n */\nexport function logApiCall(\n logger: Logger,\n method: string,\n path: string,\n options?: {\n statusCode?: number;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n method,\n path,\n ...options,\n };\n\n if (options?.statusCode) {\n extra['statusCode'] = options.statusCode;\n }\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `API call failed: ${method} ${path}`, extra);\n } else if (options?.statusCode && options.statusCode >= 400) {\n logger.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);\n } else {\n logger.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);\n }\n}\n\n/**\n * Log an MCP tool invocation.\n */\nexport function logToolCall(\n logger: Logger,\n toolName: string,\n options?: {\n success?: boolean;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n toolName,\n ...options,\n };\n\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `Tool call failed: ${toolName}`, extra);\n } else if (options?.success === false) {\n logger.warning(`Tool call returned error: ${toolName}`, extra);\n } else {\n logger.info(`Tool call: ${toolName}`, extra);\n }\n}\n","#!/usr/bin/env node\n/**\n * Natural Payments MCP CLI — thin stdio-to-HTTP proxy.\n *\n * Proxies MCP stdio transport to the hosted Natural MCP server.\n */\n\nimport { Command } from 'commander';\nimport { startStdioServer, ServerType } from 'mcp-proxy';\nimport { configureLogging, getLogger, type LogLevel } from '../logging.js';\nimport { VERSION } from '../version.js';\n\nconst logger = getLogger('mcp.cli');\n\nconst DEFAULT_MCP_URL = 'https://mcp.natural.co';\n\nconst program = new Command();\n\nprogram.name('naturalpay').description('Natural Payments SDK CLI').version(VERSION);\n\nconst mcpCommand = program.command('mcp').description('MCP server commands');\n\nmcpCommand\n .command('serve')\n .description('Start the MCP proxy')\n .option('-k, --api-key <key>', 'API key (overrides NATURAL_API_KEY)')\n .option('-u, --mcp-url <url>', 'MCP server URL (overrides NATURAL_MCP_URL)')\n .option('-l, --log-level <level>', 'Log level: debug, info, warning, error', 'info')\n .option('--log-format <format>', 'Log format: text, json', 'text')\n .action(async (options) => {\n configureLogging({\n level: options.logLevel as LogLevel,\n jsonFormat: options.logFormat === 'json',\n });\n\n const url = options.mcpUrl ?? process.env['NATURAL_MCP_URL'] ?? DEFAULT_MCP_URL;\n const apiKey = options.apiKey ?? process.env['NATURAL_API_KEY'];\n\n if (!apiKey) {\n logger.error('No API key provided. Set NATURAL_API_KEY or use --api-key.');\n process.exit(1);\n }\n\n logger.info('Starting Natural Payments MCP proxy', { url });\n\n await startStdioServer({\n url,\n serverType: ServerType.HTTPStream,\n transportOptions: {\n requestInit: {\n headers: { Authorization: `Bearer ${apiKey}` },\n },\n },\n });\n });\n\nprogram.exitOverride((err) => {\n if (err.code === 'commander.missingMandatoryOptionValue') {\n logger.error(`Missing required option: ${err.message}`);\n }\n process.exit(1);\n});\n\nprogram.parse();\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/version.ts","../../src/logging.ts","../../src/mcp/cli.ts"],"names":["AsyncLocalStorage","Command","startStdioServer","ServerType"],"mappings":"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","file":"cli.cjs","sourcesContent":["/**\n * Single source of truth for the SDK version string.\n */\nexport const VERSION = '0.1.3';\n","/**\n * Structured logging for Natural Payments SDK.\n *\n * Supports two modes:\n * 1. Plain text logging (default, for local development)\n * 2. JSON logging with Datadog correlation (when NATURAL_LOG_FORMAT=json)\n *\n * Features:\n * - Source info: file, line, function on every log (for error grouping)\n * - Context binding for request_id, agent_id, instance_id\n * - Datadog trace correlation fields\n * - AsyncLocalStorage for proper async context isolation\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\nimport { VERSION } from './version.js';\n\n// Log levels (matching standard levels)\nexport type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nconst LOG_LEVEL_VALUES: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warning: 30,\n error: 40,\n};\n\n// AsyncLocalStorage for proper async context isolation\nconst asyncContext = new AsyncLocalStorage<Record<string, unknown>>();\n\n// Fallback global context for environments where AsyncLocalStorage isn't used\nlet globalContext: Record<string, unknown> = {};\n\n/**\n * Get the current logging context.\n * Uses AsyncLocalStorage if available, falls back to global context.\n */\nexport function getContext(): Record<string, unknown> {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n return { ...asyncStore };\n }\n return { ...globalContext };\n}\n\n/**\n * Sanitize a string for safe logging to prevent log injection.\n */\nfunction sanitizeString(value: string): string {\n let sanitized = '';\n for (const char of value) {\n if (char === '\\r' || char === '\\n') {\n sanitized += '\\\\n';\n } else if (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) {\n // Control characters - escape them\n sanitized += `\\\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`;\n } else {\n sanitized += char;\n }\n }\n // Truncate overly long values to prevent log flooding\n const maxLength = 1000;\n if (sanitized.length > maxLength) {\n sanitized = sanitized.slice(0, maxLength) + '...[truncated]';\n }\n return sanitized;\n}\n\n/**\n * Sanitize a value for safe logging to prevent log injection.\n *\n * Recursively sanitizes strings in objects and arrays.\n * Removes/escapes control characters, newlines, and other potentially\n * dangerous sequences that could be used for log injection attacks.\n */\nfunction sanitizeLogValue(value: unknown, depth = 0): unknown {\n // Prevent infinite recursion on deeply nested objects\n const maxDepth = 10;\n if (depth > maxDepth) {\n return '[max depth exceeded]';\n }\n\n if (value === null || value === undefined) {\n return value;\n }\n\n if (typeof value === 'string') {\n return sanitizeString(value);\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeLogValue(item, depth + 1));\n }\n\n if (typeof value === 'object') {\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const sanitizedKey = sanitizeString(key);\n sanitized[sanitizedKey] = sanitizeLogValue(val, depth + 1);\n }\n return sanitized;\n }\n\n // For functions, symbols, etc. - return type description\n return `[${typeof value}]`;\n}\n\n/**\n * Bind additional context to current scope (e.g., request_id, agent_id).\n *\n * Values are sanitized to prevent log injection attacks.\n * Updates both AsyncLocalStorage (if active) and global context.\n *\n * @example\n * bindContext({ requestId: 'req_123', agentId: 'agt_456' });\n * logger.info('Processing payment'); // Will include requestId and agentId\n */\nexport function bindContext(context: Record<string, unknown>): void {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n\n // Update AsyncLocalStorage if active\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n Object.assign(asyncStore, sanitized);\n }\n\n // Always update global context as fallback\n globalContext = { ...globalContext, ...sanitized };\n}\n\n/**\n * Clear all bound context.\n */\nexport function clearContext(): void {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n for (const key of Object.keys(asyncStore)) {\n delete asyncStore[key];\n }\n }\n globalContext = {};\n}\n\n/**\n * Run a function with isolated logging context.\n *\n * Context bound within the callback is isolated from other async operations.\n * This is the recommended way to set context for request handling.\n *\n * @example\n * await runWithContext({ requestId: 'req_123' }, async () => {\n * logger.info('Processing request'); // Includes requestId\n * await doAsyncWork();\n * logger.info('Request complete'); // Still includes requestId\n * });\n */\nexport function runWithContext<T>(\n context: Record<string, unknown>,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n return asyncContext.run(sanitized, fn);\n}\n\n// Logging configuration\ninterface LoggingConfig {\n level: LogLevel;\n jsonFormat: boolean;\n serviceName: string;\n environment: string;\n}\n\nconst loggingConfig: LoggingConfig = {\n level: (process.env['NATURAL_LOG_LEVEL']?.toLowerCase() as LogLevel) || 'info',\n jsonFormat: process.env['NATURAL_LOG_FORMAT']?.toLowerCase() === 'json',\n serviceName: 'naturalpay-sdk',\n environment: process.env['NATURAL_ENV'] || process.env['DD_ENV'] || 'development',\n};\n\n/**\n * Configure logging for the Natural SDK.\n *\n * @param options - Logging configuration options\n * @param options.level - Log level (debug, info, warning, error)\n * @param options.jsonFormat - Use JSON format (default: auto-detect from NATURAL_LOG_FORMAT env var)\n * @param options.serviceName - Service name for structured logs\n *\n * Environment variables:\n * - NATURAL_LOG_LEVEL: Override log level (DEBUG, INFO, WARNING, ERROR)\n * - NATURAL_LOG_FORMAT: Set to \"json\" for JSON output, \"text\" for plain text\n */\nexport function configureLogging(options?: {\n level?: LogLevel;\n jsonFormat?: boolean;\n serviceName?: string;\n}): void {\n if (options?.level) {\n loggingConfig.level = options.level;\n }\n if (options?.jsonFormat !== undefined) {\n loggingConfig.jsonFormat = options.jsonFormat;\n }\n if (options?.serviceName) {\n loggingConfig.serviceName = options.serviceName;\n }\n}\n\n/**\n * Get source location information from stack trace.\n */\nfunction getSourceInfo(): { file: string; line: number; function: string } {\n const stack = new Error().stack;\n if (!stack) {\n return { file: 'unknown', line: 0, function: 'unknown' };\n }\n\n // Parse stack trace - skip the first 3 lines (Error, getSourceInfo, formatLogRecord/log)\n const lines = stack.split('\\n');\n const callerLine = lines[4] || lines[3] || '';\n\n // Match patterns like \"at functionName (/path/to/file.ts:123:45)\" or \"at /path/to/file.ts:123:45\"\n const match = callerLine.match(/at\\s+(?:(.+?)\\s+\\()?(.*?):(\\d+):\\d+\\)?/);\n if (match) {\n return {\n function: match[1] || 'anonymous',\n file: match[2] || 'unknown',\n line: parseInt(match[3] || '0', 10),\n };\n }\n\n return { file: 'unknown', line: 0, function: 'unknown' };\n}\n\ninterface LogRecord {\n timestamp: string;\n level: string;\n logger: string;\n message: string;\n source: { file: string; line: number; function: string };\n service: string;\n environment: string;\n version: string;\n [key: string]: unknown;\n}\n\n/**\n * Format a log record as JSON.\n */\nfunction formatJsonLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const record: LogRecord = {\n timestamp: new Date().toISOString(),\n level: level.toUpperCase(),\n logger: loggerName,\n message,\n source: getSourceInfo(),\n service: loggingConfig.serviceName,\n environment: loggingConfig.environment,\n version: VERSION,\n ...getContext(),\n ...extra,\n };\n\n // Add Datadog trace correlation if available\n const ddTraceId = process.env['DD_TRACE_ID'];\n const ddSpanId = process.env['DD_SPAN_ID'];\n if (ddTraceId) {\n record['dd.trace_id'] = ddTraceId;\n record['dd.span_id'] = ddSpanId;\n record['dd.service'] = loggingConfig.serviceName;\n record['dd.version'] = VERSION;\n record['dd.env'] = loggingConfig.environment;\n }\n\n return JSON.stringify(record);\n}\n\n/**\n * Format a log record as plain text.\n */\nfunction formatTextLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const contextEntries = Object.entries({ ...getContext(), ...extra });\n const contextStr =\n contextEntries.length > 0 ? ` [${contextEntries.map(([k, v]) => `${k}=${v}`).join(', ')}]` : '';\n\n return `${level.toUpperCase().padEnd(8)} ${loggerName}: ${message}${contextStr}`;\n}\n\n/**\n * Logger instance for a specific module/context.\n */\nexport class Logger {\n constructor(private readonly name: string) {}\n\n private log(level: LogLevel, message: string, extra?: Record<string, unknown>): void {\n if (LOG_LEVEL_VALUES[level] < LOG_LEVEL_VALUES[loggingConfig.level]) {\n return;\n }\n\n const formatted = loggingConfig.jsonFormat\n ? formatJsonLog(level, this.name, message, extra)\n : formatTextLog(level, this.name, message, extra);\n\n // Use stderr for logs (best practice for CLI tools)\n if (level === 'error') {\n console.error(formatted);\n } else if (level === 'warning') {\n console.warn(formatted);\n } else {\n console.error(formatted); // All logs to stderr\n }\n }\n\n debug(message: string, extra?: Record<string, unknown>): void {\n this.log('debug', message, extra);\n }\n\n info(message: string, extra?: Record<string, unknown>): void {\n this.log('info', message, extra);\n }\n\n warning(message: string, extra?: Record<string, unknown>): void {\n this.log('warning', message, extra);\n }\n\n warn(message: string, extra?: Record<string, unknown>): void {\n this.warning(message, extra);\n }\n\n error(message: string, extra?: Record<string, unknown>): void {\n this.log('error', message, extra);\n }\n}\n\n/**\n * Get a logger for the given module name.\n *\n * @param name - Module name (e.g., 'naturalpay.http')\n * @returns Logger instance\n *\n * @example\n * const logger = getLogger('naturalpay.payments');\n * logger.info('Payment initiated', { amount: 10000 });\n */\nexport function getLogger(name: string): Logger {\n if (!name.startsWith('naturalpay')) {\n name = `naturalpay.${name}`;\n }\n return new Logger(name);\n}\n\n// Helper functions for common logging patterns\n\n/**\n * Log an error with full context and exception info.\n */\nexport function logError(\n logger: Logger,\n message: string,\n options?: {\n error?: Error;\n statusCode?: number;\n code?: string;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = { ...options };\n\n if (options?.error) {\n extra['errorType'] = options.error.name;\n extra['errorMessage'] = options.error.message;\n if (options.error.stack) {\n extra['errorStack'] = options.error.stack.split('\\n').slice(0, 5).join('\\n');\n }\n delete extra['error'];\n }\n\n logger.error(message, extra);\n}\n\n/**\n * Log an API call with standard fields.\n */\nexport function logApiCall(\n logger: Logger,\n method: string,\n path: string,\n options?: {\n statusCode?: number;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n method,\n path,\n ...options,\n };\n\n if (options?.statusCode) {\n extra['statusCode'] = options.statusCode;\n }\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `API call failed: ${method} ${path}`, extra);\n } else if (options?.statusCode && options.statusCode >= 400) {\n logger.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);\n } else {\n logger.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);\n }\n}\n\n/**\n * Log an MCP tool invocation.\n */\nexport function logToolCall(\n logger: Logger,\n toolName: string,\n options?: {\n success?: boolean;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n toolName,\n ...options,\n };\n\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `Tool call failed: ${toolName}`, extra);\n } else if (options?.success === false) {\n logger.warning(`Tool call returned error: ${toolName}`, extra);\n } else {\n logger.info(`Tool call: ${toolName}`, extra);\n }\n}\n","#!/usr/bin/env node\n/**\n * Natural Payments MCP CLI — thin stdio-to-HTTP proxy.\n *\n * Proxies MCP stdio transport to the hosted Natural MCP server.\n */\n\nimport { Command } from 'commander';\nimport { startStdioServer, ServerType } from 'mcp-proxy';\nimport { configureLogging, getLogger, type LogLevel } from '../logging.js';\nimport { VERSION } from '../version.js';\n\nconst logger = getLogger('mcp.cli');\n\nconst DEFAULT_MCP_URL = 'https://mcp.natural.co';\n\nconst program = new Command();\n\nprogram.name('naturalpay').description('Natural Payments SDK CLI').version(VERSION);\n\nconst mcpCommand = program.command('mcp').description('MCP server commands');\n\nmcpCommand\n .command('serve')\n .description('Start the MCP proxy')\n .option('-k, --api-key <key>', 'API key (overrides NATURAL_API_KEY)')\n .option('-u, --mcp-url <url>', 'MCP server URL (overrides NATURAL_MCP_URL)')\n .option('-l, --log-level <level>', 'Log level: debug, info, warning, error', 'info')\n .option('--log-format <format>', 'Log format: text, json', 'text')\n .action(async (options) => {\n configureLogging({\n level: options.logLevel as LogLevel,\n jsonFormat: options.logFormat === 'json',\n });\n\n const url = options.mcpUrl ?? process.env['NATURAL_MCP_URL'] ?? DEFAULT_MCP_URL;\n const apiKey = options.apiKey ?? process.env['NATURAL_API_KEY'];\n\n if (!apiKey) {\n logger.error('No API key provided. Set NATURAL_API_KEY or use --api-key.');\n process.exit(1);\n }\n\n logger.info('Starting Natural Payments MCP proxy', { url });\n\n await startStdioServer({\n url,\n serverType: ServerType.HTTPStream,\n transportOptions: {\n requestInit: {\n headers: { Authorization: `Bearer ${apiKey}` },\n },\n },\n });\n });\n\nprogram.exitOverride((err) => {\n if (err.code === 'commander.missingMandatoryOptionValue') {\n logger.error(`Missing required option: ${err.message}`);\n }\n process.exit(1);\n});\n\nprogram.parse();\n"]}
|
package/dist/mcp/cli.js
CHANGED
package/dist/mcp/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/version.ts","../../src/logging.ts","../../src/mcp/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Single source of truth for the SDK version string.\n */\nexport const VERSION = '0.1.1';\n","/**\n * Structured logging for Natural Payments SDK.\n *\n * Supports two modes:\n * 1. Plain text logging (default, for local development)\n * 2. JSON logging with Datadog correlation (when NATURAL_LOG_FORMAT=json)\n *\n * Features:\n * - Source info: file, line, function on every log (for error grouping)\n * - Context binding for request_id, agent_id, instance_id\n * - Datadog trace correlation fields\n * - AsyncLocalStorage for proper async context isolation\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\nimport { VERSION } from './version.js';\n\n// Log levels (matching standard levels)\nexport type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nconst LOG_LEVEL_VALUES: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warning: 30,\n error: 40,\n};\n\n// AsyncLocalStorage for proper async context isolation\nconst asyncContext = new AsyncLocalStorage<Record<string, unknown>>();\n\n// Fallback global context for environments where AsyncLocalStorage isn't used\nlet globalContext: Record<string, unknown> = {};\n\n/**\n * Get the current logging context.\n * Uses AsyncLocalStorage if available, falls back to global context.\n */\nexport function getContext(): Record<string, unknown> {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n return { ...asyncStore };\n }\n return { ...globalContext };\n}\n\n/**\n * Sanitize a string for safe logging to prevent log injection.\n */\nfunction sanitizeString(value: string): string {\n let sanitized = '';\n for (const char of value) {\n if (char === '\\r' || char === '\\n') {\n sanitized += '\\\\n';\n } else if (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) {\n // Control characters - escape them\n sanitized += `\\\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`;\n } else {\n sanitized += char;\n }\n }\n // Truncate overly long values to prevent log flooding\n const maxLength = 1000;\n if (sanitized.length > maxLength) {\n sanitized = sanitized.slice(0, maxLength) + '...[truncated]';\n }\n return sanitized;\n}\n\n/**\n * Sanitize a value for safe logging to prevent log injection.\n *\n * Recursively sanitizes strings in objects and arrays.\n * Removes/escapes control characters, newlines, and other potentially\n * dangerous sequences that could be used for log injection attacks.\n */\nfunction sanitizeLogValue(value: unknown, depth = 0): unknown {\n // Prevent infinite recursion on deeply nested objects\n const maxDepth = 10;\n if (depth > maxDepth) {\n return '[max depth exceeded]';\n }\n\n if (value === null || value === undefined) {\n return value;\n }\n\n if (typeof value === 'string') {\n return sanitizeString(value);\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeLogValue(item, depth + 1));\n }\n\n if (typeof value === 'object') {\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const sanitizedKey = sanitizeString(key);\n sanitized[sanitizedKey] = sanitizeLogValue(val, depth + 1);\n }\n return sanitized;\n }\n\n // For functions, symbols, etc. - return type description\n return `[${typeof value}]`;\n}\n\n/**\n * Bind additional context to current scope (e.g., request_id, agent_id).\n *\n * Values are sanitized to prevent log injection attacks.\n * Updates both AsyncLocalStorage (if active) and global context.\n *\n * @example\n * bindContext({ requestId: 'req_123', agentId: 'agt_456' });\n * logger.info('Processing payment'); // Will include requestId and agentId\n */\nexport function bindContext(context: Record<string, unknown>): void {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n\n // Update AsyncLocalStorage if active\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n Object.assign(asyncStore, sanitized);\n }\n\n // Always update global context as fallback\n globalContext = { ...globalContext, ...sanitized };\n}\n\n/**\n * Clear all bound context.\n */\nexport function clearContext(): void {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n for (const key of Object.keys(asyncStore)) {\n delete asyncStore[key];\n }\n }\n globalContext = {};\n}\n\n/**\n * Run a function with isolated logging context.\n *\n * Context bound within the callback is isolated from other async operations.\n * This is the recommended way to set context for request handling.\n *\n * @example\n * await runWithContext({ requestId: 'req_123' }, async () => {\n * logger.info('Processing request'); // Includes requestId\n * await doAsyncWork();\n * logger.info('Request complete'); // Still includes requestId\n * });\n */\nexport function runWithContext<T>(\n context: Record<string, unknown>,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n return asyncContext.run(sanitized, fn);\n}\n\n// Logging configuration\ninterface LoggingConfig {\n level: LogLevel;\n jsonFormat: boolean;\n serviceName: string;\n environment: string;\n}\n\nconst loggingConfig: LoggingConfig = {\n level: (process.env['NATURAL_LOG_LEVEL']?.toLowerCase() as LogLevel) || 'info',\n jsonFormat: process.env['NATURAL_LOG_FORMAT']?.toLowerCase() === 'json',\n serviceName: 'naturalpay-sdk',\n environment: process.env['NATURAL_ENV'] || process.env['DD_ENV'] || 'development',\n};\n\n/**\n * Configure logging for the Natural SDK.\n *\n * @param options - Logging configuration options\n * @param options.level - Log level (debug, info, warning, error)\n * @param options.jsonFormat - Use JSON format (default: auto-detect from NATURAL_LOG_FORMAT env var)\n * @param options.serviceName - Service name for structured logs\n *\n * Environment variables:\n * - NATURAL_LOG_LEVEL: Override log level (DEBUG, INFO, WARNING, ERROR)\n * - NATURAL_LOG_FORMAT: Set to \"json\" for JSON output, \"text\" for plain text\n */\nexport function configureLogging(options?: {\n level?: LogLevel;\n jsonFormat?: boolean;\n serviceName?: string;\n}): void {\n if (options?.level) {\n loggingConfig.level = options.level;\n }\n if (options?.jsonFormat !== undefined) {\n loggingConfig.jsonFormat = options.jsonFormat;\n }\n if (options?.serviceName) {\n loggingConfig.serviceName = options.serviceName;\n }\n}\n\n/**\n * Get source location information from stack trace.\n */\nfunction getSourceInfo(): { file: string; line: number; function: string } {\n const stack = new Error().stack;\n if (!stack) {\n return { file: 'unknown', line: 0, function: 'unknown' };\n }\n\n // Parse stack trace - skip the first 3 lines (Error, getSourceInfo, formatLogRecord/log)\n const lines = stack.split('\\n');\n const callerLine = lines[4] || lines[3] || '';\n\n // Match patterns like \"at functionName (/path/to/file.ts:123:45)\" or \"at /path/to/file.ts:123:45\"\n const match = callerLine.match(/at\\s+(?:(.+?)\\s+\\()?(.*?):(\\d+):\\d+\\)?/);\n if (match) {\n return {\n function: match[1] || 'anonymous',\n file: match[2] || 'unknown',\n line: parseInt(match[3] || '0', 10),\n };\n }\n\n return { file: 'unknown', line: 0, function: 'unknown' };\n}\n\ninterface LogRecord {\n timestamp: string;\n level: string;\n logger: string;\n message: string;\n source: { file: string; line: number; function: string };\n service: string;\n environment: string;\n version: string;\n [key: string]: unknown;\n}\n\n/**\n * Format a log record as JSON.\n */\nfunction formatJsonLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const record: LogRecord = {\n timestamp: new Date().toISOString(),\n level: level.toUpperCase(),\n logger: loggerName,\n message,\n source: getSourceInfo(),\n service: loggingConfig.serviceName,\n environment: loggingConfig.environment,\n version: VERSION,\n ...getContext(),\n ...extra,\n };\n\n // Add Datadog trace correlation if available\n const ddTraceId = process.env['DD_TRACE_ID'];\n const ddSpanId = process.env['DD_SPAN_ID'];\n if (ddTraceId) {\n record['dd.trace_id'] = ddTraceId;\n record['dd.span_id'] = ddSpanId;\n record['dd.service'] = loggingConfig.serviceName;\n record['dd.version'] = VERSION;\n record['dd.env'] = loggingConfig.environment;\n }\n\n return JSON.stringify(record);\n}\n\n/**\n * Format a log record as plain text.\n */\nfunction formatTextLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const contextEntries = Object.entries({ ...getContext(), ...extra });\n const contextStr =\n contextEntries.length > 0 ? ` [${contextEntries.map(([k, v]) => `${k}=${v}`).join(', ')}]` : '';\n\n return `${level.toUpperCase().padEnd(8)} ${loggerName}: ${message}${contextStr}`;\n}\n\n/**\n * Logger instance for a specific module/context.\n */\nexport class Logger {\n constructor(private readonly name: string) {}\n\n private log(level: LogLevel, message: string, extra?: Record<string, unknown>): void {\n if (LOG_LEVEL_VALUES[level] < LOG_LEVEL_VALUES[loggingConfig.level]) {\n return;\n }\n\n const formatted = loggingConfig.jsonFormat\n ? formatJsonLog(level, this.name, message, extra)\n : formatTextLog(level, this.name, message, extra);\n\n // Use stderr for logs (best practice for CLI tools)\n if (level === 'error') {\n console.error(formatted);\n } else if (level === 'warning') {\n console.warn(formatted);\n } else {\n console.error(formatted); // All logs to stderr\n }\n }\n\n debug(message: string, extra?: Record<string, unknown>): void {\n this.log('debug', message, extra);\n }\n\n info(message: string, extra?: Record<string, unknown>): void {\n this.log('info', message, extra);\n }\n\n warning(message: string, extra?: Record<string, unknown>): void {\n this.log('warning', message, extra);\n }\n\n warn(message: string, extra?: Record<string, unknown>): void {\n this.warning(message, extra);\n }\n\n error(message: string, extra?: Record<string, unknown>): void {\n this.log('error', message, extra);\n }\n}\n\n/**\n * Get a logger for the given module name.\n *\n * @param name - Module name (e.g., 'naturalpay.http')\n * @returns Logger instance\n *\n * @example\n * const logger = getLogger('naturalpay.payments');\n * logger.info('Payment initiated', { amount: 10000 });\n */\nexport function getLogger(name: string): Logger {\n if (!name.startsWith('naturalpay')) {\n name = `naturalpay.${name}`;\n }\n return new Logger(name);\n}\n\n// Helper functions for common logging patterns\n\n/**\n * Log an error with full context and exception info.\n */\nexport function logError(\n logger: Logger,\n message: string,\n options?: {\n error?: Error;\n statusCode?: number;\n code?: string;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = { ...options };\n\n if (options?.error) {\n extra['errorType'] = options.error.name;\n extra['errorMessage'] = options.error.message;\n if (options.error.stack) {\n extra['errorStack'] = options.error.stack.split('\\n').slice(0, 5).join('\\n');\n }\n delete extra['error'];\n }\n\n logger.error(message, extra);\n}\n\n/**\n * Log an API call with standard fields.\n */\nexport function logApiCall(\n logger: Logger,\n method: string,\n path: string,\n options?: {\n statusCode?: number;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n method,\n path,\n ...options,\n };\n\n if (options?.statusCode) {\n extra['statusCode'] = options.statusCode;\n }\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `API call failed: ${method} ${path}`, extra);\n } else if (options?.statusCode && options.statusCode >= 400) {\n logger.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);\n } else {\n logger.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);\n }\n}\n\n/**\n * Log an MCP tool invocation.\n */\nexport function logToolCall(\n logger: Logger,\n toolName: string,\n options?: {\n success?: boolean;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n toolName,\n ...options,\n };\n\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `Tool call failed: ${toolName}`, extra);\n } else if (options?.success === false) {\n logger.warning(`Tool call returned error: ${toolName}`, extra);\n } else {\n logger.info(`Tool call: ${toolName}`, extra);\n }\n}\n","#!/usr/bin/env node\n/**\n * Natural Payments MCP CLI — thin stdio-to-HTTP proxy.\n *\n * Proxies MCP stdio transport to the hosted Natural MCP server.\n */\n\nimport { Command } from 'commander';\nimport { startStdioServer, ServerType } from 'mcp-proxy';\nimport { configureLogging, getLogger, type LogLevel } from '../logging.js';\nimport { VERSION } from '../version.js';\n\nconst logger = getLogger('mcp.cli');\n\nconst DEFAULT_MCP_URL = 'https://mcp.natural.co';\n\nconst program = new Command();\n\nprogram.name('naturalpay').description('Natural Payments SDK CLI').version(VERSION);\n\nconst mcpCommand = program.command('mcp').description('MCP server commands');\n\nmcpCommand\n .command('serve')\n .description('Start the MCP proxy')\n .option('-k, --api-key <key>', 'API key (overrides NATURAL_API_KEY)')\n .option('-u, --mcp-url <url>', 'MCP server URL (overrides NATURAL_MCP_URL)')\n .option('-l, --log-level <level>', 'Log level: debug, info, warning, error', 'info')\n .option('--log-format <format>', 'Log format: text, json', 'text')\n .action(async (options) => {\n configureLogging({\n level: options.logLevel as LogLevel,\n jsonFormat: options.logFormat === 'json',\n });\n\n const url = options.mcpUrl ?? process.env['NATURAL_MCP_URL'] ?? DEFAULT_MCP_URL;\n const apiKey = options.apiKey ?? process.env['NATURAL_API_KEY'];\n\n if (!apiKey) {\n logger.error('No API key provided. Set NATURAL_API_KEY or use --api-key.');\n process.exit(1);\n }\n\n logger.info('Starting Natural Payments MCP proxy', { url });\n\n await startStdioServer({\n url,\n serverType: ServerType.HTTPStream,\n transportOptions: {\n requestInit: {\n headers: { Authorization: `Bearer ${apiKey}` },\n },\n },\n });\n });\n\nprogram.exitOverride((err) => {\n if (err.code === 'commander.missingMandatoryOptionValue') {\n logger.error(`Missing required option: ${err.message}`);\n }\n process.exit(1);\n});\n\nprogram.parse();\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/version.ts","../../src/logging.ts","../../src/mcp/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Single source of truth for the SDK version string.\n */\nexport const VERSION = '0.1.3';\n","/**\n * Structured logging for Natural Payments SDK.\n *\n * Supports two modes:\n * 1. Plain text logging (default, for local development)\n * 2. JSON logging with Datadog correlation (when NATURAL_LOG_FORMAT=json)\n *\n * Features:\n * - Source info: file, line, function on every log (for error grouping)\n * - Context binding for request_id, agent_id, instance_id\n * - Datadog trace correlation fields\n * - AsyncLocalStorage for proper async context isolation\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\nimport { VERSION } from './version.js';\n\n// Log levels (matching standard levels)\nexport type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nconst LOG_LEVEL_VALUES: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warning: 30,\n error: 40,\n};\n\n// AsyncLocalStorage for proper async context isolation\nconst asyncContext = new AsyncLocalStorage<Record<string, unknown>>();\n\n// Fallback global context for environments where AsyncLocalStorage isn't used\nlet globalContext: Record<string, unknown> = {};\n\n/**\n * Get the current logging context.\n * Uses AsyncLocalStorage if available, falls back to global context.\n */\nexport function getContext(): Record<string, unknown> {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n return { ...asyncStore };\n }\n return { ...globalContext };\n}\n\n/**\n * Sanitize a string for safe logging to prevent log injection.\n */\nfunction sanitizeString(value: string): string {\n let sanitized = '';\n for (const char of value) {\n if (char === '\\r' || char === '\\n') {\n sanitized += '\\\\n';\n } else if (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) {\n // Control characters - escape them\n sanitized += `\\\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`;\n } else {\n sanitized += char;\n }\n }\n // Truncate overly long values to prevent log flooding\n const maxLength = 1000;\n if (sanitized.length > maxLength) {\n sanitized = sanitized.slice(0, maxLength) + '...[truncated]';\n }\n return sanitized;\n}\n\n/**\n * Sanitize a value for safe logging to prevent log injection.\n *\n * Recursively sanitizes strings in objects and arrays.\n * Removes/escapes control characters, newlines, and other potentially\n * dangerous sequences that could be used for log injection attacks.\n */\nfunction sanitizeLogValue(value: unknown, depth = 0): unknown {\n // Prevent infinite recursion on deeply nested objects\n const maxDepth = 10;\n if (depth > maxDepth) {\n return '[max depth exceeded]';\n }\n\n if (value === null || value === undefined) {\n return value;\n }\n\n if (typeof value === 'string') {\n return sanitizeString(value);\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeLogValue(item, depth + 1));\n }\n\n if (typeof value === 'object') {\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n const sanitizedKey = sanitizeString(key);\n sanitized[sanitizedKey] = sanitizeLogValue(val, depth + 1);\n }\n return sanitized;\n }\n\n // For functions, symbols, etc. - return type description\n return `[${typeof value}]`;\n}\n\n/**\n * Bind additional context to current scope (e.g., request_id, agent_id).\n *\n * Values are sanitized to prevent log injection attacks.\n * Updates both AsyncLocalStorage (if active) and global context.\n *\n * @example\n * bindContext({ requestId: 'req_123', agentId: 'agt_456' });\n * logger.info('Processing payment'); // Will include requestId and agentId\n */\nexport function bindContext(context: Record<string, unknown>): void {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n\n // Update AsyncLocalStorage if active\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n Object.assign(asyncStore, sanitized);\n }\n\n // Always update global context as fallback\n globalContext = { ...globalContext, ...sanitized };\n}\n\n/**\n * Clear all bound context.\n */\nexport function clearContext(): void {\n const asyncStore = asyncContext.getStore();\n if (asyncStore) {\n for (const key of Object.keys(asyncStore)) {\n delete asyncStore[key];\n }\n }\n globalContext = {};\n}\n\n/**\n * Run a function with isolated logging context.\n *\n * Context bound within the callback is isolated from other async operations.\n * This is the recommended way to set context for request handling.\n *\n * @example\n * await runWithContext({ requestId: 'req_123' }, async () => {\n * logger.info('Processing request'); // Includes requestId\n * await doAsyncWork();\n * logger.info('Request complete'); // Still includes requestId\n * });\n */\nexport function runWithContext<T>(\n context: Record<string, unknown>,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n const sanitized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(context)) {\n sanitized[key] = sanitizeLogValue(value);\n }\n return asyncContext.run(sanitized, fn);\n}\n\n// Logging configuration\ninterface LoggingConfig {\n level: LogLevel;\n jsonFormat: boolean;\n serviceName: string;\n environment: string;\n}\n\nconst loggingConfig: LoggingConfig = {\n level: (process.env['NATURAL_LOG_LEVEL']?.toLowerCase() as LogLevel) || 'info',\n jsonFormat: process.env['NATURAL_LOG_FORMAT']?.toLowerCase() === 'json',\n serviceName: 'naturalpay-sdk',\n environment: process.env['NATURAL_ENV'] || process.env['DD_ENV'] || 'development',\n};\n\n/**\n * Configure logging for the Natural SDK.\n *\n * @param options - Logging configuration options\n * @param options.level - Log level (debug, info, warning, error)\n * @param options.jsonFormat - Use JSON format (default: auto-detect from NATURAL_LOG_FORMAT env var)\n * @param options.serviceName - Service name for structured logs\n *\n * Environment variables:\n * - NATURAL_LOG_LEVEL: Override log level (DEBUG, INFO, WARNING, ERROR)\n * - NATURAL_LOG_FORMAT: Set to \"json\" for JSON output, \"text\" for plain text\n */\nexport function configureLogging(options?: {\n level?: LogLevel;\n jsonFormat?: boolean;\n serviceName?: string;\n}): void {\n if (options?.level) {\n loggingConfig.level = options.level;\n }\n if (options?.jsonFormat !== undefined) {\n loggingConfig.jsonFormat = options.jsonFormat;\n }\n if (options?.serviceName) {\n loggingConfig.serviceName = options.serviceName;\n }\n}\n\n/**\n * Get source location information from stack trace.\n */\nfunction getSourceInfo(): { file: string; line: number; function: string } {\n const stack = new Error().stack;\n if (!stack) {\n return { file: 'unknown', line: 0, function: 'unknown' };\n }\n\n // Parse stack trace - skip the first 3 lines (Error, getSourceInfo, formatLogRecord/log)\n const lines = stack.split('\\n');\n const callerLine = lines[4] || lines[3] || '';\n\n // Match patterns like \"at functionName (/path/to/file.ts:123:45)\" or \"at /path/to/file.ts:123:45\"\n const match = callerLine.match(/at\\s+(?:(.+?)\\s+\\()?(.*?):(\\d+):\\d+\\)?/);\n if (match) {\n return {\n function: match[1] || 'anonymous',\n file: match[2] || 'unknown',\n line: parseInt(match[3] || '0', 10),\n };\n }\n\n return { file: 'unknown', line: 0, function: 'unknown' };\n}\n\ninterface LogRecord {\n timestamp: string;\n level: string;\n logger: string;\n message: string;\n source: { file: string; line: number; function: string };\n service: string;\n environment: string;\n version: string;\n [key: string]: unknown;\n}\n\n/**\n * Format a log record as JSON.\n */\nfunction formatJsonLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const record: LogRecord = {\n timestamp: new Date().toISOString(),\n level: level.toUpperCase(),\n logger: loggerName,\n message,\n source: getSourceInfo(),\n service: loggingConfig.serviceName,\n environment: loggingConfig.environment,\n version: VERSION,\n ...getContext(),\n ...extra,\n };\n\n // Add Datadog trace correlation if available\n const ddTraceId = process.env['DD_TRACE_ID'];\n const ddSpanId = process.env['DD_SPAN_ID'];\n if (ddTraceId) {\n record['dd.trace_id'] = ddTraceId;\n record['dd.span_id'] = ddSpanId;\n record['dd.service'] = loggingConfig.serviceName;\n record['dd.version'] = VERSION;\n record['dd.env'] = loggingConfig.environment;\n }\n\n return JSON.stringify(record);\n}\n\n/**\n * Format a log record as plain text.\n */\nfunction formatTextLog(\n level: LogLevel,\n loggerName: string,\n message: string,\n extra?: Record<string, unknown>\n): string {\n const contextEntries = Object.entries({ ...getContext(), ...extra });\n const contextStr =\n contextEntries.length > 0 ? ` [${contextEntries.map(([k, v]) => `${k}=${v}`).join(', ')}]` : '';\n\n return `${level.toUpperCase().padEnd(8)} ${loggerName}: ${message}${contextStr}`;\n}\n\n/**\n * Logger instance for a specific module/context.\n */\nexport class Logger {\n constructor(private readonly name: string) {}\n\n private log(level: LogLevel, message: string, extra?: Record<string, unknown>): void {\n if (LOG_LEVEL_VALUES[level] < LOG_LEVEL_VALUES[loggingConfig.level]) {\n return;\n }\n\n const formatted = loggingConfig.jsonFormat\n ? formatJsonLog(level, this.name, message, extra)\n : formatTextLog(level, this.name, message, extra);\n\n // Use stderr for logs (best practice for CLI tools)\n if (level === 'error') {\n console.error(formatted);\n } else if (level === 'warning') {\n console.warn(formatted);\n } else {\n console.error(formatted); // All logs to stderr\n }\n }\n\n debug(message: string, extra?: Record<string, unknown>): void {\n this.log('debug', message, extra);\n }\n\n info(message: string, extra?: Record<string, unknown>): void {\n this.log('info', message, extra);\n }\n\n warning(message: string, extra?: Record<string, unknown>): void {\n this.log('warning', message, extra);\n }\n\n warn(message: string, extra?: Record<string, unknown>): void {\n this.warning(message, extra);\n }\n\n error(message: string, extra?: Record<string, unknown>): void {\n this.log('error', message, extra);\n }\n}\n\n/**\n * Get a logger for the given module name.\n *\n * @param name - Module name (e.g., 'naturalpay.http')\n * @returns Logger instance\n *\n * @example\n * const logger = getLogger('naturalpay.payments');\n * logger.info('Payment initiated', { amount: 10000 });\n */\nexport function getLogger(name: string): Logger {\n if (!name.startsWith('naturalpay')) {\n name = `naturalpay.${name}`;\n }\n return new Logger(name);\n}\n\n// Helper functions for common logging patterns\n\n/**\n * Log an error with full context and exception info.\n */\nexport function logError(\n logger: Logger,\n message: string,\n options?: {\n error?: Error;\n statusCode?: number;\n code?: string;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = { ...options };\n\n if (options?.error) {\n extra['errorType'] = options.error.name;\n extra['errorMessage'] = options.error.message;\n if (options.error.stack) {\n extra['errorStack'] = options.error.stack.split('\\n').slice(0, 5).join('\\n');\n }\n delete extra['error'];\n }\n\n logger.error(message, extra);\n}\n\n/**\n * Log an API call with standard fields.\n */\nexport function logApiCall(\n logger: Logger,\n method: string,\n path: string,\n options?: {\n statusCode?: number;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n method,\n path,\n ...options,\n };\n\n if (options?.statusCode) {\n extra['statusCode'] = options.statusCode;\n }\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `API call failed: ${method} ${path}`, extra);\n } else if (options?.statusCode && options.statusCode >= 400) {\n logger.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);\n } else {\n logger.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);\n }\n}\n\n/**\n * Log an MCP tool invocation.\n */\nexport function logToolCall(\n logger: Logger,\n toolName: string,\n options?: {\n success?: boolean;\n durationMs?: number;\n error?: Error;\n [key: string]: unknown;\n }\n): void {\n const extra: Record<string, unknown> = {\n toolName,\n ...options,\n };\n\n if (options?.durationMs !== undefined) {\n extra['durationMs'] = Math.round(options.durationMs * 100) / 100;\n }\n\n if (options?.error) {\n logError(logger, `Tool call failed: ${toolName}`, extra);\n } else if (options?.success === false) {\n logger.warning(`Tool call returned error: ${toolName}`, extra);\n } else {\n logger.info(`Tool call: ${toolName}`, extra);\n }\n}\n","#!/usr/bin/env node\n/**\n * Natural Payments MCP CLI — thin stdio-to-HTTP proxy.\n *\n * Proxies MCP stdio transport to the hosted Natural MCP server.\n */\n\nimport { Command } from 'commander';\nimport { startStdioServer, ServerType } from 'mcp-proxy';\nimport { configureLogging, getLogger, type LogLevel } from '../logging.js';\nimport { VERSION } from '../version.js';\n\nconst logger = getLogger('mcp.cli');\n\nconst DEFAULT_MCP_URL = 'https://mcp.natural.co';\n\nconst program = new Command();\n\nprogram.name('naturalpay').description('Natural Payments SDK CLI').version(VERSION);\n\nconst mcpCommand = program.command('mcp').description('MCP server commands');\n\nmcpCommand\n .command('serve')\n .description('Start the MCP proxy')\n .option('-k, --api-key <key>', 'API key (overrides NATURAL_API_KEY)')\n .option('-u, --mcp-url <url>', 'MCP server URL (overrides NATURAL_MCP_URL)')\n .option('-l, --log-level <level>', 'Log level: debug, info, warning, error', 'info')\n .option('--log-format <format>', 'Log format: text, json', 'text')\n .action(async (options) => {\n configureLogging({\n level: options.logLevel as LogLevel,\n jsonFormat: options.logFormat === 'json',\n });\n\n const url = options.mcpUrl ?? process.env['NATURAL_MCP_URL'] ?? DEFAULT_MCP_URL;\n const apiKey = options.apiKey ?? process.env['NATURAL_API_KEY'];\n\n if (!apiKey) {\n logger.error('No API key provided. Set NATURAL_API_KEY or use --api-key.');\n process.exit(1);\n }\n\n logger.info('Starting Natural Payments MCP proxy', { url });\n\n await startStdioServer({\n url,\n serverType: ServerType.HTTPStream,\n transportOptions: {\n requestInit: {\n headers: { Authorization: `Bearer ${apiKey}` },\n },\n },\n });\n });\n\nprogram.exitOverride((err) => {\n if (err.code === 'commander.missingMandatoryOptionValue') {\n logger.error(`Missing required option: ${err.message}`);\n }\n process.exit(1);\n});\n\nprogram.parse();\n"]}
|